[User]
I'm looking to create a customizable plant tag, and I've found a design that's pretty close to what I want, but I'd like to see if we can make some improvements. I need an OpenSCAD file that lets me specify the common name, variety name, and botanical name of a plant, and have them printed on the tag. I'd like to be able to adjust the width, height, and thickness of the rectangular tag part, as well as the length and width of the stake that goes into the ground. The text should be raised off the surface of the tag, and I need to be able to adjust the thickness of the raised text.

The existing design has a few extra features that I think I want: an option for a border around the tag and also an option for a reinforcing ridge down the center of the stake. I'd like to keep both of those options, if possible.

Finally, I need to be able to adjust the font used for each of the three names and their sizes, as well as adjust their vertical positions and the spacing between letters. I want a few preselected font choices as well as the option to use a custom font. I'm okay with that custom font needing to be specified via a separate text field.

I'd like the stick portion to have a slight taper and some rounded edges, so it's easy to push into the soil and also not as likely to break. I'm hoping that the parameters that dictate the rounded portions can also be adjusted. The main tag itself should also have rounded corners.

I see that the existing design uses `linear_extrude`, `offset`, `polygon`, `minkowski`, `cube`, `cylinder`, `text`, `difference`, `union`, `intersection`, and `rotate`. I'm expecting that my design would need to use these as well, along with `$fn` for the rounded parts. I also noticed that it uses `color()`, which would be useful for previewing.

I'm not sure how the font selection is done, but I noticed the existing design uses a string array to select the font by index. It'd be ideal if this could be avoided.

[Assistant]
PlantTag-V1.4.scad:
```
/* Plant Tag
   by Lyl3

Creates a simple plant tag with a common name, variety name, and botanical name.

This code is published at:
https://www.thingiverse.com/thing:4793066
licensed under the Creative Commons - Attribution - Share Alike license (CC BY-SA 3.0)

V0.1 - basic functioning code
V0.2 - simplified text positioning
V0.3 - set some sane limits on the parameters
V0.4 - added parameters for variety name
V0.5 - tuned the spacing of the text
V0.6 - added a selection list of "specially selected" fonts
V0.7 - added parameters for adjusting the font spacing
V0.8 - corrected label thickness, compensating for the fudge factor of the Z location
V1.0 - ready for release
V1.1 - added a parameter for specifying the stake length
V1.2 - added an option for a reinforcing ridge down the middle of the stick the same height as the text
V1.3 - added an option for a border around the tag the same height as the text
V1.4 - added a parameter for specifying the stake width, allowed smaller tag width and font sizes
*/

/* [Plant Name Parameters] */
common_name = "Common Name";
variety_name = "Variety Name";
botanical_name = "Botanical name";

/* [Tag Parameters] */
// Specify the tag width (mm).
tag_width = 80; // [20:1:120]
// Specify the tag height (the rectanglular part) (mm).
tag_height = 20; // [6.0:1:60.0]
// Specify the tag thickness (mm).
tag_thickness = 1.0; // [0.6:0.01:5]
// Specify the length of the tag stake (mm).
stake_length = 40; // [10:100]
// Specify the width of the tag stake (mm).
stake_width = 5; // [2:30]
// Specify the label thickness (mm).
label_thickness = 0.6; // [0.2:0.01:3]
// Add a border around the label?
border = "no"; // [yes,no]
// Reinforce the stick with a ridge down the center the same height as the text?
reinforce = "no"; // [yes,no]

/* [Basic Font Parameters] */
// Some suitable fonts with italic styles
//"Arial";            // Microsoft font not available on Thingiverse customizer or Google Fonts
//"Liberation Sans";  // Curved joints on the glyphs are thin
//"Nunito Sans";      // Has tight kerning
//"Raleway";          // Has descended numbers
//"Roboto";           // No issues
//"Mali";             // An informal font with very even thickness on the glyphs
//"Montserrat";       // A font with very even thickness on the glyphs
//"Muli";
//"Niramit";
//"Rosario";
//"Rubik";
//"Ubuntu"

// Choose from a small selection of fonts that should work well for 3D printing small text. The black style, bold style, and bold italic style will be used for the common name, the variety name, and the botanical name respectively. (NOTE: Arial is not available on Thingiverse).
fontName = 1; // [1:Arial, 2:Nunito Sans, 3:Raleway, 4:Roboto, 5:Mali, 6:Montserrat, 0:other - specified in Advanced Font Parameters section ]

// Specify the font size for printing the common name of the plant.
common_name_font_size = 6; // [1:0.1:20]
// Specify the font size for printing the variety name of the plant.
variety_name_font_size = 4.6; // [1:0.1:20]
// Specify the font size for printing the botanical name of the plant.
botanical_name_font_size = 4.2; // [1:0.1:20]

/* [Advanced Font Parameters] */
// If "other" is selected for the font name in the Basic Parameters section, specify the font here to use for printing the common name of the plant.
common_name_font = "Liberation Sans:style=Bold";
// If "other" is selected for the font name in the Basic Parameters section, specify the font here to use for printing the variety name of the plant.
variety_name_font = "Liberation Sans:style=Bold";
// If "other" is selected for the font name in the Basic Parameters section, specify the font here to use for printing the botanical name of the plant.
botanical_name_font = "Liberation Sans:style=Bold Italic";
// If the position needs adjusting, specify the distance to shift up the common name of the plant (mm). A negative value shifts it down.
common_name_shift_up = -0.5; // [-3:0.1:10]
// If the position needs adjusting, specify the distance to shift up the variety name of the plant (mm). A negative value shifts it down.
variety_name_shift_up = -0.5; // [-3:0.1:10]
// If the position needs adjusting, specify the distance to shift up the botanical name of the plant (mm). A negative value shifts it down.
botanical_name_shift_up = 0.2; // [-2:0.1:10]
// If the spacing between the letters needs to be adjusted, specify the value. A value of 1 is normal spacing for the font. A larger value increases the spacing and a smaller value decreases the spacing.
common_name_spacing = 1; // [0.5:0.01:2]
// If the spacing between the letters needs to be adjusted, specify the value. A value of 1 is normal spacing for the font. A larger value increases the spacing and a smaller value decreases the spacing.
variety_name_spacing = 1; // [0.5:0.01:2]
// If the spacing between the letters needs to be adjusted, specify the value. A value of 1 is normal spacing for the font. A larger value increases the spacing and a smaller value decreases the spacing.
botanical_name_spacing = 1; // [0.5:0.01:2]

/* [Hidden] */
$fn=50;
fudge = 0.01;
round_radius = 2;

// This convoluted way of selecting the font from an array was a requirement introduced by a change
// on the Thingiverse customizer that broke the ability to directly select fonts with a style specified.
// This issue was first noticed 2010-02-26 and probably happened within the two months prior to that date.
commonName_fontNames = ["Ski-bi dibby dib yo da dub dub Yo da dub dub"
  , "Arial Black:style=Regular"
  , "Nunito Sans:style=Black"
  , "Raleway:style=Black"
  , "Roboto:style=Black"
  , "Mali:style=Bold"
  , "Montserrat:style=Bold"];
varietyName_fontNames = ["Ba-da-ba-da-ba-be bop bop bodda bope Bop ba bodda bope"
  , "Arial:style=Bold"
  , "Nunito Sans:style=Bold"
  , "Raleway:style=Bold"
  , "Roboto:style=Bold"
  , "Mali:style=Bold"
  , "Montserrat:style=Bold"];
botanicalName_fontNames = ["Ska-badabadabadoo-belidabbelydabbladabbladabblabab-belibabbelibabbelibabbelabbelo-doobelidoo"
  , "Arial:style=Bold Italic"
  , "Nunito Sans:style=Bold Italic"
  , "Raleway:style=Bold Italic"
  , "Roboto:style=Bold Italic"
  , "Mali:style=Bold Italic"
  , "Montserrat:style=Bold Italic"];

selectedCommonNameFont = (fontName==0) ? common_name_font : commonName_fontNames[fontName];
selectedVarietyNameFont = (fontName==0) ? variety_name_font : varietyName_fontNames[fontName];
selectedBotanicalNameFont = (fontName==0) ? botanical_name_font : botanicalName_fontNames[fontName];

commonNameSpace = (common_name =="") ? 0 : common_name_font_size;
varietyNameSpace = (variety_name =="") ? 0 : variety_name_font_size;
botanicalNameSpace = (botanical_name =="") ? 0 : botanical_name_font_size;
totalNameSpace = commonNameSpace + varietyNameSpace + botanicalNameSpace;
commonNameYloc = (tag_height/totalNameSpace) * 1.0 * (botanicalNameSpace + varietyNameSpace + commonNameSpace/2);
varietyNameYloc = (tag_height/totalNameSpace) * 1.0 * (botanicalNameSpace + varietyNameSpace/2);
botanicalNameYloc = (tag_height/totalNameSpace) * 1.0 * botanicalNameSpace/2;

echo (commonNameSpace, varietyNameSpace, botanicalNameSpace, totalNameSpace, commonNameYloc, varietyNameYloc, botanicalNameYloc);

// Set the stick coordinates
s_off = 1;                    // stick offset (to make fillet)
s_top = stake_width/2 + 4.5 + s_off;            // half of stick top width
s_mid = stake_width/2 + 2.5 + s_off;            // half of stick middle width
s_bot = stake_width/2 + s_off;          // half of stick bottom width
s_len = stake_length - 4.25;  // stick length

tag_points = [ [-s_top,0], [-s_top,s_off*2+2], [s_top,s_off*2+2], [s_top,0], [s_mid,0], [s_bot,-4], [s_bot,-s_len], [0,-s_len-5], [-s_bot,-s_len], [-s_bot,-4], [-s_mid,0] ];
tag_paths = [ [0,1,2,3,4,5,6,7,8,9,10] ];

{
  // Create the stick
  color("OliveDrab") translate([0,-s_off,0]) linear_extrude(tag_thickness) offset(r=-s_off) polygon(tag_points,tag_paths,1);
  // Create the reinforcing wall on the stick
  if (reinforce == "yes") {
    color("Red") translate([0,0,tag_thickness-fudge]) union() {
      intersection() {
        translate([-1.25,-stake_length,0]) cube([2.5,(border == "yes") ? stake_length: s_len,label_thickness]);
        translate([0,-s_off,0]) linear_extrude(label_thickness) offset(r=-s_off) polygon(tag_points,tag_paths,1);
      }
      if (border == "yes") {
        translate([0,-1.8,0]) linear_extrude(label_thickness) rotate([0,0,-90]) circle (4.5, $fn=3);
      }
    }
  }
  // Create the label area
  color("OliveDrab") translate([-tag_width/2+round_radius,round_radius,0]) minkowski()
  {
    cube([tag_width-2*round_radius,tag_height-2*round_radius,tag_thickness/2]);
    cylinder(r=round_radius,h=tag_thickness/2);
  }
  // Create the border around the label area
  if (border == "yes") {
    translate([0,0,tag_thickness-fudge]) color("Red") difference() {
      translate([-tag_width/2+round_radius,round_radius,0]) minkowski()
      {
        cube([tag_width-2*round_radius,tag_height-2*round_radius,label_thickness/2]);
        cylinder(r=round_radius,h=label_thickness/2);
      }
      translate([-tag_width/2+round_radius+0.8,round_radius+0.8,-fudge/2]) minkowski()
      {
        cube([tag_width-2*round_radius-1.6,tag_height-2*round_radius-1.6,label_thickness/2+fudge*2]);
        cylinder(r=round_radius,h=label_thickness/2);
      }
    }
  }
}

// Create the text
translate([0,commonNameYloc+common_name_shift_up,tag_thickness-fudge]) color("red") linear_extrude(label_thickness+fudge) text(common_name, size=common_name_font_size, halign="center", valign="center", font=selectedCommonNameFont, spacing=common_name_spacing);

translate([0,varietyNameYloc+variety_name_shift_up,tag_thickness-fudge]) color("red") linear_extrude(label_thickness+fudge) text(variety_name, size=variety_name_font_size, halign="center", valign="center", font=selectedVarietyNameFont, spacing=variety_name_spacing);

translate([0,botanicalNameYloc+botanical_name_shift_up,tag_thickness-fudge]) color("red") linear_extrude(label_thickness+fudge) text(botanical_name, size=botanical_name_font_size, halign="center", valign="center", font=selectedBotanicalNameFont, spacing=botanical_name_spacing);

```
PlantTag-V1.3.scad:
```
/* Plant Tag
   by Lyl3

Creates a simple plant tag with a common name, variety name, and botanical name.

This code is published at:
https://www.thingiverse.com/thing:4793066
licensed under the Creative Commons - Attribution - Share Alike license (CC BY-SA 3.0)

V0.1 - basic functioning code
V0.2 - simplified text positioning
V0.3 - set some sane limits on the parameters
V0.4 - added parameters for variety name
V0.5 - tuned the spacing of the text
V0.6 - added a selection list of "specially selected" fonts
V0.7 - added parameters for adjusting the font spacing
V0.8 - corrected label thickness, compensating for the fudge factor of the Z location
V1.0 - ready for release
V1.1 - added a parameter for specifying the stake length
V1.2 - added an option for a reinforcing ridge down the middle of the stick the same height as the text
V1.3 - added an option for a border around the tag the same height as the text
*/

/* [Plant Name Parameters] */
common_name = "Common Name";
variety_name = "Variety Name";
botanical_name = "Botanical name";

/* [Tag Parameters] */
// Specify the tag width (mm).
tag_width = 80; // [30:1:120]
// Specify the tag height (the rectanglular part) (mm).
tag_height = 20; // [6.0:1:60.0]
// Specify the tag thickness (mm).
tag_thickness = 1.0; // [0.6:0.01:5]
// Specify the length of the tag stake (mm).
stake_length = 40; // [10:100]
// Specify the label thickness (mm).
label_thickness = 0.6; // [0.2:0.01:3]
// Add a border around the label?
border = "no"; // [yes,no]
// Reinforce the stick with a ridge down the center the same height as the text?
reinforce = "no"; // [yes,no]

/* [Basic Font Parameters] */
// Some suitable fonts with italic styles
//"Arial";            // Not available on Thingiverse customizer or Google Fonts
//"Liberation Sans";  // Curved joints on the glyphs are thin
//"Nunito Sans";      // Has tight kerning
//"Raleway";          // Has descended numbers
//"Roboto";           // No issues
//"Mali";             // An informal font with very even thickness on the glyphs
//"Montserrat";       // A font with very even thickness on the glyphs
//"Muli";
//"Niramit";
//"Rosario";
//"Rubik";
//"Ubuntu"

// Choose from a small selection of fonts that should work well for 3D printing small text. The black style, bold style, and bold italic style will be used for the common name, the variety name, and the botanical name respectively. (NOTE: Arial is not available on Thingiverse).
fontName = 1; // [1:Arial, 2:Nunito Sans, 3:Raleway, 4:Roboto, 5:Mali, 6:Montserrat, 0:other ]

// Specify the font size for printing the common name of the plant.
common_name_font_size = 6; // [3:0.1:20]
// Specify the font size for printing the variety name of the plant.
variety_name_font_size = 4.6; // [3:0.1:20]
// Specify the font size for printing the botanical name of the plant.
botanical_name_font_size = 4.2; // [3:0.1:20]

/* [Advanced Font Parameters] */
// If "other" is selected for the font name in the Basic Parameters section, specify the font here to use for printing the common name of the plant.
common_name_font = "Liberation Sans:style=Bold";
// If "other" is selected for the font name in the Basic Parameters section, specify the font here to use for printing the variety name of the plant.
variety_name_font = "Liberation Sans:style=Bold";
// If "other" is selected for the font name in the Basic Parameters section, specify the font here to use for printing the botanical name of the plant.
botanical_name_font = "Liberation Sans:style=Bold Italic";
// If the position needs adjusting, specify the distance to shift up the common name of the plant (mm). A negative value shifts it down.
common_name_shift_up = -0.5; // [-3:0.1:10]
// If the position needs adjusting, specify the distance to shift up the variety name of the plant (mm). A negative value shifts it down.
variety_name_shift_up = -0.5; // [-3:0.1:10]
// If the position needs adjusting, specify the distance to shift up the botanical name of the plant (mm). A negative value shifts it down.
botanical_name_shift_up = 0.2; // [-2:0.1:10]
// If the spacing between the letters needs to be adjusted, specify the value. A value of 1 is normal spacing for the font. A larger value increases the spacing and a smaller value decreases the spacing.
common_name_spacing = 1; // [0.5:0.01:2]
// If the spacing between the letters needs to be adjusted, specify the value. A value of 1 is normal spacing for the font. A larger value increases the spacing and a smaller value decreases the spacing.
variety_name_spacing = 1; // [0.5:0.01:2]
// If the spacing between the letters needs to be adjusted, specify the value. A value of 1 is normal spacing for the font. A larger value increases the spacing and a smaller value decreases the spacing.
botanical_name_spacing = 1; // [0.5:0.01:2]

/* [Hidden] */
$fn=50;
fudge = 0.01;
round_radius = 2;

// This convoluted way of selecting the font from an array was a requirement introduced by a change
// on the Thingiverse customizer that broke the ability to directly select fonts with a style specified.
// This issue was first noticed 2010-02-26 and probably happened within the two months prior to that date.
commonName_fontNames = ["Ski-bi dibby dib yo da dub dub Yo da dub dub"
  , "Arial Black:style=Regular"
  , "Nunito Sans:style=Black"
  , "Raleway:style=Black"
  , "Roboto:style=Black"
  , "Mali:style=Bold"
  , "Montserrat:style=Bold"];
varietyName_fontNames = ["Ba-da-ba-da-ba-be bop bop bodda bope Bop ba bodda bope"
  , "Arial:style=Bold"
  , "Nunito Sans:style=Bold"
  , "Raleway:style=Bold"
  , "Roboto:style=Bold"
  , "Mali:style=Bold"
  , "Montserrat:style=Bold"];
botanicalName_fontNames = ["Ska-badabadabadoo-belidabbelydabbladabbladabblabab-belibabbelibabbelibabbelabbelo-doobelidoo"
  , "Arial:style=Bold Italic"
  , "Nunito Sans:style=Bold Italic"
  , "Raleway:style=Bold Italic"
  , "Roboto:style=Bold Italic"
  , "Mali:style=Bold Italic"
  , "Montserrat:style=Bold Italic"];

selectedCommonNameFont = (fontName==0) ? common_name_font : commonName_fontNames[fontName];
selectedVarietyNameFont = (fontName==0) ? variety_name_font : varietyName_fontNames[fontName];
selectedBotanicalNameFont = (fontName==0) ? botanical_name_font : botanicalName_fontNames[fontName];

commonNameSpace = (common_name =="") ? 0 : common_name_font_size;
varietyNameSpace = (variety_name =="") ? 0 : variety_name_font_size;
botanicalNameSpace = (botanical_name =="") ? 0 : botanical_name_font_size;
totalNameSpace = commonNameSpace + varietyNameSpace + botanicalNameSpace;
commonNameYloc = (tag_height/totalNameSpace) * 1.0 * (botanicalNameSpace + varietyNameSpace + commonNameSpace/2);
varietyNameYloc = (tag_height/totalNameSpace) * 1.0 * (botanicalNameSpace + varietyNameSpace/2);
botanicalNameYloc = (tag_height/totalNameSpace) * 1.0 * botanicalNameSpace/2;

echo (commonNameSpace, varietyNameSpace, botanicalNameSpace, totalNameSpace, commonNameYloc, varietyNameYloc, botanicalNameYloc);

// Set the stick coordinates
s_off = 1;                    // stick offset (to make fillet)
s_top = 7 + s_off;            // half of stick top width
s_mid = 5 + s_off;            // half of stick middle width
s_bot = 2.5 + s_off;          // half of stick bottom width
s_len = stake_length - 4.25;  // stick length

tag_points = [ [-s_top,0], [-s_top,s_off*2+2], [s_top,s_off*2+2], [s_top,0], [s_mid,0], [s_bot,-4], [s_bot,-s_len], [0,-s_len-5], [-s_bot,-s_len], [-s_bot,-4], [-s_mid,0] ];
tag_paths = [ [0,1,2,3,4,5,6,7,8,9,10] ];

{
  // Create the stick
  color("OliveDrab") translate([0,-s_off,0]) linear_extrude(tag_thickness) offset(r=-s_off) polygon(tag_points,tag_paths,1);
  // Create the reinforcing wall on the stick
  if (reinforce == "yes") {
    color("Red") translate([0,0,tag_thickness-fudge]) union() {
      intersection() {
        translate([-1.25,-stake_length,0]) cube([2.5,(border == "yes") ? stake_length: s_len,label_thickness]);
        translate([0,-s_off,0]) linear_extrude(label_thickness) offset(r=-s_off) polygon(tag_points,tag_paths,1);
      }
      if (border == "yes") {
        translate([0,-1.8,0]) linear_extrude(label_thickness) rotate([0,0,-90]) circle (4.5, $fn=3);
      }
    }
  }
  // Create the label area
  color("OliveDrab") translate([-tag_width/2+round_radius,round_radius,0]) minkowski()
  {
    cube([tag_width-2*round_radius,tag_height-2*round_radius,tag_thickness/2]);
    cylinder(r=round_radius,h=tag_thickness/2);
  }
  // Create the border around the label area
  if (border == "yes") {
    translate([0,0,tag_thickness-fudge]) color("Red") difference() {
      translate([-tag_width/2+round_radius,round_radius,0]) minkowski()
      {
        cube([tag_width-2*round_radius,tag_height-2*round_radius,label_thickness/2]);
        cylinder(r=round_radius,h=label_thickness/2);
      }
      translate([-tag_width/2+round_radius+0.8,round_radius+0.8,-fudge/2]) minkowski()
      {
        cube([tag_width-2*round_radius-1.6,tag_height-2*round_radius-1.6,label_thickness/2+fudge*2]);
        cylinder(r=round_radius,h=label_thickness/2);
      }
    }
  }
}

// Create the text
translate([0,commonNameYloc+common_name_shift_up,tag_thickness-fudge]) color("red") linear_extrude(label_thickness+fudge) text(common_name, size=common_name_font_size, halign="center", valign="center", font=selectedCommonNameFont, spacing=common_name_spacing);

translate([0,varietyNameYloc+variety_name_shift_up,tag_thickness-fudge]) color("red") linear_extrude(label_thickness+fudge) text(variety_name, size=variety_name_font_size, halign="center", valign="center", font=selectedVarietyNameFont, spacing=variety_name_spacing);

translate([0,botanicalNameYloc+botanical_name_shift_up,tag_thickness-fudge]) color("red") linear_extrude(label_thickness+fudge) text(botanical_name, size=botanical_name_font_size, halign="center", valign="center", font=selectedBotanicalNameFont, spacing=botanical_name_spacing);

```


[User]
Hey, I'm looking to design some parts for a DIY air purifier project, and I'd love some help getting started with the OpenSCAD code. I've got a basic idea, but I need a parametric design that can handle a few different configurations.

Basically, I need three types of parts: filter connectors, fan connectors, and intermediate connectors. The filter connectors will attach to different filter types. The fan connectors need to support different sized fans. The connectors are cylindrical and need to fit the filter connectors on one end and the fan connectors on the other.

For filter connectors, I'm currently focusing on one filter type initially, the Xiaomi SCG4021GL filter. These connectors will have a two-tiered design; the bottom tier will be designed to snugly fit the filter, and the top tier will connect to an intermediate connector. I'll need to specify the outer and inner radii and height for each tier.

For fan connectors, I want to support three sizes: 120x120x25mm, 140x140x25mm, and 200x200x30mm. These connectors will also connect to the intermediate connector. They'll be primarily a cylindrical shape. And I'll need to include mounting holes for the different fan sizes.

The intermediate connectors will be between the filter and fan parts. These will transition from the filter connector on one end to the fan connector on the other end. They should be simple cylindrical connectors, with the option of variable heights.

It would be great if you could create a main OpenSCAD file that includes the definitions of all the parts. I'd also like it to use a modular approach, so it's easy to update or add new parts later. It would be useful to have the ability to specify fan type and filter type using parameters at the start of the main file. I am using standard cylinders for these parts, but I'd like to avoid the use of a single overall cylinder that has holes removed, especially for the fan connector, so that we can print these in a way that avoids overhangs and needs minimal support. I don't need any specialized libraries like MCAD or BOSL for this.

Thanks in advance!

[Assistant]
OAPPv4.scad:
```
/* [Part] */
part = "filter_connector"; // [filter_connector:Filter Connector,connector:Connector,fan_connector:Fan Connector]
/* [Fan] */
Fan_selector=2; // [0:120x120x25mm, 1:140x140x25mm, 2:200x200x30mm]
/* [Filter] */
Filter_selector=0; // [0:Xiaomi SCG4021GL, 1:Don't click here]
/* [Hidden] */
Fan_sizes = [
            [[120],[25],[105.0],[58.5]],
            [[140],[25],[124.5],[67.5]],
            [[200],[30],[170.0],[96.5]]
            ];
current_Fan_size = Fan_sizes[Fan_selector][0][0];
current_Fan_height = Fan_sizes[Fan_selector][1][0];
current_Fan_drill_hole_spacing = Fan_sizes[Fan_selector][2][0];
current_Fan_Inner_Radius = Fan_sizes[Fan_selector][3][0];
Filter_connector = [
                    [[105],[72.5],[5],[105],[100],[5]],
                    [[0],[0],[0],[0],[0],[0]]
                   ];
current_Filter_Outer_Radius_A = Filter_connector[Filter_selector][0][0];
current_Filter_Inner_Radius_A = Filter_connector[Filter_selector][1][0];
current_Filter_Height_A = Filter_connector[Filter_selector][2][0];
current_Filter_Outer_Radius_B = Filter_connector[Filter_selector][3][0];
current_Filter_Inner_Radius_B = Filter_connector[Filter_selector][4][0];
current_Filter_Height_B = Filter_connector[Filter_selector][5][0];
Outer_Bottom_Radius_C= current_Filter_Inner_Radius_A + 5;
Inner_Bottom_Radius_C= current_Filter_Inner_Radius_A;
Outer_Top_Radius_C = current_Fan_Inner_Radius + 5;
Inner_Top_Radius_C = current_Fan_Inner_Radius;
Outer_Bottom_Radius_D= current_Fan_Inner_Radius + 35;
Inner_Bottom_Radius_D= current_Fan_Inner_Radius;
Outer_Top_Radius_D = current_Fan_Inner_Radius + 35;
Inner_Top_Radius_D = current_Fan_Inner_Radius;

/* [Part Filter Connector Settings] */
current_Filter_Inner_Fragments = 36;
current_Filter_Outer_Fragments = 36;

/* [Part Connector Settings] */
Height_C= 100;
Inner_Fragments_C = 36;
Outer_Fragments_C = 36;

/* [Part Fan Connector Settings] */
Height_D= 5;
Inner_Fragments_D = 36;
Outer_Fragments_D = 8;

print_part();

module print_part() {
	if (part == "filter_connector") {
		filter_connector();
	} else if (part == "connector") {
		connector();
	} else if (part == "fan_connector") {
		fan_connector();
	} else {
		fan_connector();
	}
}

module filter_connector() {
    translate([0,0,current_Filter_Height_A/2])
    {
        difference() {
            cylinder(h = current_Filter_Height_A, r1 = current_Filter_Outer_Radius_A, r2 = current_Filter_Outer_Radius_A, center = true, $fn = current_Filter_Outer_Fragments);
            cylinder(h = current_Filter_Height_A, r1 = current_Filter_Inner_Radius_A, r2 = current_Filter_Inner_Radius_A, center = true, $fn = current_Filter_Inner_Fragments);
        }
    }
    translate([0,0,current_Filter_Height_A + current_Filter_Height_B/2])
    {
        difference() {
            cylinder(h = current_Filter_Height_B, r1 = current_Filter_Outer_Radius_B, r2 = current_Filter_Outer_Radius_B, center = true, $fn = 36);
            cylinder(h = current_Filter_Height_B, r1 = current_Filter_Inner_Radius_B, r2 = current_Filter_Inner_Radius_B, center = true, $fn = 36);
        }
    }
}

module connector() {
    translate([0,0,Height_C/2])
    {
        difference() {
            cylinder(h = Height_C, r1 = Outer_Bottom_Radius_C, r2 = Outer_Top_Radius_C, center = true, , $fn = Outer_Fragments_C);
            cylinder(h = Height_C, r1 = Inner_Bottom_Radius_C, r2 = Inner_Top_Radius_C, center = true, $fn = Inner_Fragments_C);
        }
    }
}

module fan_connector() {
    difference() {
        translate([0,0,Height_D/2])
        {
            difference() {
                cylinder(h = Height_D, r1 = Outer_Bottom_Radius_D, r2 = Outer_Top_Radius_D, center = true, , $fn = Outer_Fragments_D);
                cylinder(h = Height_D, r1 = Inner_Bottom_Radius_D, r2 = Inner_Top_Radius_D, center = true, $fn = Inner_Fragments_D);
            }
        }
        DrillHoles = [[current_Fan_drill_hole_spacing/2, current_Fan_drill_hole_spacing/2],[-current_Fan_drill_hole_spacing/2,current_Fan_drill_hole_spacing/2],[current_Fan_drill_hole_spacing/2,-current_Fan_drill_hole_spacing/2],[-current_Fan_drill_hole_spacing/2,-current_Fan_drill_hole_spacing/2]];
        for (a = [ 0 : len(DrillHoles) - 1 ])
        {
              point=DrillHoles[a];
              translate([point[0],point[1],0])
              {
                  cylinder(r=2.15,h=100,$fn = 36);
              }
        }
   }
}

```


[User]
Hi! I'm looking to get some OpenSCAD files created for a customizable model train bridge, specifically for Kato Unitrack. I have some existing code, but I'd like to start fresh and do it in a more parametric and modular fashion, if possible.

Here's what I need:

The bridge should be a curved section, designed to fit a specific radius of Kato Unitrack. I need to be able to control:

1.  **Curve Radius:** This should be selectable, with options matching common Kato radii (315mm, 348mm, 381mm, and 481mm).
2.  **Arc Angle:** The length of the curved section in degrees (e.g. 10, 15, 20, 30, 45 degrees, maybe more).
3.  **Deck Width and Offset:** I want to be able to set the width of the deck itself and how far the deck extends from the track's edge. This will allow for the creation of single-track or dual-track bridges.
4.  **Side Height:**  The height of the walls or sides of the bridge.
5.  **Track Width:** This should be fixed at 25mm for Kato Unitrack, but the code should use a variable for flexibility just in case.

I'd like the generated model to be solid and printable. I’m thinking of something similar to the Kato Viaduct bridges, but with a flat bottom for easier printing and placement.

Essentially, the bridge will be created from several concentric arcs, with walls along the outside edges. There should be two main arcs for the inner and outer edge of the track, a slightly wider arc for the deck (which has an offset from the track edge) and a final set of arcs for the walls on the edges of the deck. The curves should be built in a modular way so that it's easy to add different deck styles later.

I think the key to making this clean will be to have some good parameterized modules that can create each of these elements based on input variables, and then the core file will just use these modules. I'm not looking for any fancy details or textures at this stage, just a basic functional bridge. I've also not been too successful with customizers on thingiverse, so would much rather get a .scad file to allow me to change things manually.

I don't need the AI to use any specific library. I'd prefer it be as simple as possible with minimal dependencies.

Thanks!

[Assistant]
UnitrackCurvedTrainBridge.scad:
```

// Customizable Curved Bridge for Kato Unitrack
//

// Radius of Curve (mm) [12 3/8", 13 3/4", 15", 19"]
Curve_Radius = 381; // [315, 348, 381, 481]

// Curve Length in Degrees
Arc_Deg = 20; // [10, 15, 20, 30, 45]

// Deck Offset
Deck_Offset = 3; // [1:20]

// Height of sides
Side_Height = 8; // [2:15]

// Track Width
Track_Width = 25;

module __Customizer_Limit__ () {}

track_width_2 = Track_Width/2;
function i2mm(in) = in * 25.4;

translate([-Curve_Radius, 0, 0]) {

    arc(Curve_Radius+track_width_2, Arc_Deg, 1, 5, .3);
    arc(Curve_Radius-track_width_2, Arc_Deg, Track_Width, 1, 0);
    arc(Curve_Radius-track_width_2-1, Arc_Deg, 1, 5, -.3);

    translate([0, 0, 5]) {
        arc(Curve_Radius-(track_width_2)-(Deck_Offset), Arc_Deg, Deck_Offset, 1, 0);
        arc(Curve_Radius+(track_width_2), Arc_Deg, Deck_Offset, 1, 0);
    }

    translate([0, 0, 5]) {
        arc(Curve_Radius+(track_width_2)+Deck_Offset, Arc_Deg, 1, Side_Height, .3);
        arc(Curve_Radius-(track_width_2)-Deck_Offset-1, Arc_Deg, 1, Side_Height, -.3);
    }
}

// Arc code stolen^H^H^H^H^H^H borrowed from JustinSDK:
// https://openhome.cc/eGossip/OpenSCAD/SectorArc.html
fn = 90*4;

module sector(radius, angles, fn = 24) {
    r = radius / cos(180 / fn);
    step = -360 / fn;

    points = concat([[0, 0]],
        [for(a = [angles[0] : step : angles[1] - 360])
            [r * cos(a), r * sin(a)]
        ],
        [[r * cos(angles[1]), r * sin(angles[1])]]
    );

    difference() {
        circle(radius, $fn = fn);
        polygon(points);
    }
}

module arc_inner(radius, angles, width = 1, fn = fn) {
    difference() {
        sector(radius + width, angles, fn);
        sector(radius, angles, fn);
    }
}

module arc(r, ang, w, h, ridge_offset) {
    linear_extrude(h) arc_inner(r, [0, ang], w);
    if (ridge_offset) {
        for (ridge = [0: 1: ang]) {
            //echo("ridge ", ridge);
            rotate([0, 0, ridge]) {
                translate([r+ridge_offset, 0, 0])
                    cube([1, 1, h]);
            }
        }
    }
}

```


[User]
I'm looking for an OpenSCAD design for a test tube rack, specifically for creating agar slants. I'd like it to hold the tubes at an angle while the agar cools and solidifies. I need it to be fully printable and parametric, so I can adjust the dimensions and angles to suit different tube sizes, and the amount of tubes.

Here are the core features I'm thinking of:

*   **Angled Tube Holding:** The main feature is that the tubes will be held at an angle relative to the horizontal. I'd like to specify this angle as a parameter.
*   **Grid Structure:** The rack will have a grid-like structure, with holes for each tube. This grid would be repeated to hold more tubes, and have a back plate behind the grid(s). I would like the grid structure to be modular and parametric for both horizontal and vertical hole count.
*   **Modular Parts:** I need the rack to be constructed from a few different part types which can easily slot together. I was thinking there'd be a "plate" with the tube holes, "grids" that repeat the plates to hold more tubes in the z direction, "rails" that connect the grids, and "bases" to hold up the whole thing at the desired angle.
*   **Parametric Dimensions:** I need to control things like the diameter of the tube holes, the spacing between the holes, the material thickness, the width of the base pieces, the width of the connecting rails, and the depth of slots in the base and rails.
*   **Easy Assembly:** The parts should slot together without any screws or glue, similar to how things might slot together in a kit.
*   **Customization:** I’d also like the number of grids to be configurable.

I'd like to be able to control these parameters from the customizer:
*   `tube_size` (diameter of culture tube in mm)
*   `angle` (angle from horizontal in degrees)
*  `num_holes_horiz` (Number of holes in horizontal direction)
*  `num_holes_vert` (Number of holes in vertical direction)
*   `thickness` (material thickness in mm)
*   `hole_slack` (added to tube diameter to widen holes for easier insertion in mm)
*   `hole_spacing` (space between holes or from hole to edge in mm)
*   `grid_spacing` (space between grids/back plate in mm)
*   `num_grids` (Number of grids)
*   `base_width` (width of base pieces in mm)
*   `rail_width` (width of connecting rails in mm)
*   `slot_depth` (depth of slots in base pieces and connecting rails in mm)
*   `part` (selection of which part to display; "assembly", "plate", "grid", "rail", "base")

For the final design, I'm assuming it'll use the MCAD library for units. Let me know if you need anything else!

[Assistant]
slant-rack.scad:
```
// Copyright 2021 Keegan McAllister
// License: CC-BY 4.0
// https://creativecommons.org/licenses/by/4.0/

include <MCAD/units.scad>

/* [Part selection] */

// Part selection
part = "assembly"; // [assembly, plate, grid, rail, base]

/* [Basic] */

// Diameter of culture tube (mm)
tube_size = 25;

// Angle from horizontal (degrees)
angle = 20;

// Number of holes in horizontal direction
num_holes_horiz = 4;

// Number of holes in vertical direction
num_holes_vert = 2;

/* [Advanced] */

// Material thickness (mm)
thickness = 1.5;

// Added to tube diameter to widen holes for easier insertion (mm)
hole_slack = 1.0;

// Space between holes or from hole to edge (mm)
hole_spacing = 8;

// Space between grids / back plate (mm)
grid_spacing = 30;

// Number of grids
num_grids = 2;

// Width of base pieces (mm)
base_width = 20;

// Width of connecting rails (mm)
rail_width = 10;

// Depth of slots in base pieces and connecting rails (mm)
slot_depth = 5;

eff_tube_size = tube_size + hole_slack;
hole_pitch = eff_tube_size + hole_spacing;
plate_width = num_holes_horiz*hole_pitch + hole_spacing;
plate_height = num_holes_vert*hole_pitch + hole_spacing;
hole_offset = eff_tube_size/2 + hole_spacing;
grid_pitch = grid_spacing + thickness;
rail_length = grid_pitch*num_grids + 3*thickness;

module plate() {
    cube([plate_width, plate_height, thickness]);
}

module grid() {
    difference() {
        plate();

        for (i=[0:num_holes_vert-1], j=[0:num_holes_horiz-1]) {
            translate([hole_offset + j*hole_pitch,
                       hole_offset + i*hole_pitch,
                       -epsilon])
            cylinder(h=thickness + epsilon*2, r=eff_tube_size/2);
        }
    }
}

module smooth_rail(width) {
    cube([rail_length, width, thickness]);
}

module generic_rail(width) {
    smooth_rail(width);

    for (i=[0:num_grids], d=[0, 2*thickness]) {
        translate([i*grid_pitch + d, 0, thickness])
        cube([thickness, width, slot_depth]);
    }
}

module rail() {
    generic_rail(rail_width);
}

module base() {
    rotate(-angle * Y)
    generic_rail(base_width);

    hull() {
        rotate(-angle * Y)
        smooth_rail(base_width);

        smooth_rail(base_width);
    }
}

module assembly() {
    module oriented_rail() {
        color("Red")
        translate([rail_width, thickness, plate_height + thickness])
        rotate(180*Y)
        rotate(-90*Z)
        rail();
    }

    module oriented_base() {
        color("Gold")
        rotate(-90*Z)
        base();
    }

    oriented_base();

    translate((plate_width - base_width)*X)
    oriented_base();

    rotate(-angle * X)
    translate([0, -thickness, thickness]) {
        rotate(90*X) {
            color("MediumBlue")
            plate();

            color("LimeGreen")
            for (i=[1:num_grids]) {
                translate(i*grid_pitch * Z)
                grid();
            }
        }

        oriented_rail();

        translate((plate_width - rail_width)*X)
        oriented_rail();
    }
}

     if (part == "assembly") assembly();
else if (part == "plate")    plate();
else if (part == "grid")     grid();
else if (part == "rail")     rail();
else if (part == "base")     base();
else assert(false, str("unknown part: ", part));

```


[User]
Hey, I'm looking to get an OpenSCAD design for a pool skimmer basket. I've got some specific parameters in mind and I'd like it to be parametric so I can easily adjust it for different pool skimmers.

Basically, the basket is a truncated cone with holes on the sides and bottom, and it'll be printed in two parts that fit together at an angle to avoid needing supports. I'd like it to have the option for either the top rim or the base to be detachable. Let's call the detached part the "interface part". I think it would also be nice to include some optional clips that hold the two pieces together around the edges for added support - not critical but cool!

Here are the parameters I need to control:

*   **`ENABLE_MAIN_BASKET`**: A boolean to enable/disable the main basket part.
*   **`ENABLE_PART`**: A boolean to enable/disable the interface part.
*   **`DETACH_BASE`**: A boolean to control if the base is the interface part (true) or the rim is the interface part (false).
*   **`RIM_WIDTH`**: The width (thickness) of the top rim along the XY plane.
*   **`RIM_HEIGHT`**: The height (thickness) of the top rim along the Z plane.
*   **`TOP_DIAMETER`**: The outer diameter of the basket at the top (without the rim).
*   **`BOTTOM_DIAMETER`**: The outer diameter of the basket at the bottom.
*   **`BASKET_HEIGHT`**: The total height of the basket.
*   **`BOTTOM_THICKNESS`**: The thickness of the base of the basket.
*   **`SIDE_THICKNESS`**: The thickness of the sides of the basket.
*   **`SIDE_HOLE_WIDTH`**: The width of the holes on the sides.
*   **`SIDE_HOLE_HEIGHT`**: The height of the holes on the sides.
*  **`SIDE_HOLE_XY_SPACING`**: The spacing between side holes along the outer surface.
*  **`SIDE_HOLE_Z_SPACING`**: The spacing between side holes in the Z direction.
*   **`BOTTOM_HOLE_X_SIZE`**: The width of the holes on the bottom.
*   **`BOTTOM_HOLE_Y_SIZE`**: The height of the holes on the bottom.
*   **`BOTTOM_HOLE_X_SPACING`**: The spacing between bottom holes in the X direction.
*   **`BOTTOM_HOLE_Y_SPACING`**: The spacing between bottom holes in the Y direction.
*   **`BOTTOM_HOLE_SPACING_FROM_EDGE`**:  The clearance of the inner edge of the basket and holes - although this will be automatically overridden when `DETACH_BASE` is enabled.
*   **`NUMBER_OF_PAIRS_OF_CLIPS`**: The number of pairs of clips to generate around the circumference.
*   **`HEIGHT_OF_CLIPS`**: The height of the clips.
*  **`WIDTH_OF_CLIPS`**: The width of the clips
*   **`CLIP_SPACING_FROM_INTERFACE`**: The distance of the clips from the interface.
*   **`CLIP_OVERLAP`**: How much the clip overlaps the basket.
*   **`CLIP_CYLINDER_FACES`**: The number of faces for the cylinders that make up the clips.
*   **`OFFSET_OF_CLIP_CYLINDER`**: The offset of the cylinder that is used to make the clip.
*   **`INTERFACE_ANGLE`**: The angle in degrees of the interface between the two parts.
*   **`CYLINDER_FACES`**: Number of faces for cylinders used for basket.
*   **`CUT_DELTA`**: An extra distance to cut to allow for faster rendering.

For the bottom holes, I'd like them to have an option to be offset so the holes run along the center of the base. If the user prefers a filled center, they should be able to change those offsets to 0 or anything in between.

The clips should be optional and only generated if `NUMBER_OF_PAIRS_OF_CLIPS` is greater than 0.

The file should calculate the `BOTTOM_HOLES_X_OFFSET` and `BOTTOM_HOLES_Y_OFFSET` automatically based on `BOTTOM_HOLE_X_SIZE`, `BOTTOM_HOLE_X_SPACING`, `BOTTOM_HOLE_Y_SIZE`, and `BOTTOM_HOLE_Y_SPACING`, respectively.

I think that's everything! Let me know if you have any questions. Thanks!

[Assistant]
skimmer_basket.scad:
```
//pool skimmer basket
//v1.1      -consolidated functions and code.
//          -added side clips to hold part in place (EXPERIMENTAL - untested)
//          -added offsets for base holes

//units are in mm

ENABLE_MAIN_BASKET = 1;  //enable generation of basket if > 0
ENABLE_PART = 1;  //enable generation of rim/base if > 0

DETACH_BASE = 1;  //0 for detached rim, 1 for detached base

RIM_WIDTH = 10;   //thickness of rim along XY plane at the top of the basket
RIM_HEIGHT = 6;   //thickness of rim along z plane
TOP_DIAMETER = 190;  //outer diameter at the top of basket WITHOUT RIM
BOTTOM_DIAMETER = 145; //outer diameter at bottom of basket
BASKET_HEIGHT = 130;
BOTTOM_THICKNESS = 4;  //thickness of the base of basket
SIDE_THICKNESS = 4.1;  //thickness of the sides of basket

SIDE_HOLE_WIDTH = 8;  //size of holes along the sides
SIDE_HOLE_HEIGHT = 8;

SIDE_HOLE_XY_SPACING = 6; //spacing between side holes along outer surface
SIDE_HOLE_Z_SPACING = 6;

BOTTOM_HOLE_X_SIZE = 8;  //size of holes along base
BOTTOM_HOLE_Y_SIZE = 8;
BOTTOM_HOLE_X_SPACING = 6;
BOTTOM_HOLE_Y_SPACING = 6;

BOTTOM_HOLE_SPACING_FROM_EDGE = 3;  //clearance of inner edge of basket and holes; this setting is overridden when using detacheable base

NUMBER_OF_PAIRS_OF_CLIPS = 0; //number of pairs of clips to generate
HEIGHT_OF_CLIPS = 1;
WIDTH_OF_CLIPS = 2;
CLIP_SPACING_FROM_INTERFACE = 1.75;
CLIP_OVERLAP = 0.5;  //how much the clip overlaps the basket
CLIP_CYLINDER_FACES = 36;

OFFSET_OF_CLIP_CYLINDER = HEIGHT_OF_CLIPS;

INTERFACE_ANGLE = 45; //angle in degrees of the interface between the two parts

CYLINDER_FACES = 400;
CUT_DELTA = 5; //Extra distance to cut to enable faster rendering

//the following are offsets for bottom holes such that there is holes run along the center of the base. If one prefers center to be filled, change the following to 0. Also can be any number in between.
BOTTOM_HOLES_X_OFFSET = ((BOTTOM_HOLE_X_SIZE + BOTTOM_HOLE_X_SPACING) / 2);
BOTTOM_HOLES_Y_OFFSET = ((BOTTOM_HOLE_Y_SIZE + BOTTOM_HOLE_Y_SPACING) / 2);

TOP_RADIUS = TOP_DIAMETER / 2;
BOTTOM_RADIUS = BOTTOM_DIAMETER / 2;

MIN_HEIGHT_BOTTOM_HOLES = BOTTOM_THICKNESS + SIDE_HOLE_Z_SPACING + SIDE_HOLE_HEIGHT / 2;
MIN_HEIGHT_TOP_HOLES = RIM_HEIGHT + SIDE_HOLE_Z_SPACING + SIDE_HOLE_HEIGHT / 2;

CUTOUT_RADIUS_ADJUSTMENT = (SIDE_THICKNESS - (TOP_RADIUS-BOTTOM_RADIUS) * (BOTTOM_THICKNESS / BASKET_HEIGHT));

BOTTOM_RIM_ADJUSTMENT = (TOP_RADIUS-BOTTOM_RADIUS) * (RIM_HEIGHT / BASKET_HEIGHT);

module generate_side_holes(height, radius) {

    number_of_holes = floor(radius * PI / (SIDE_HOLE_WIDTH + SIDE_HOLE_XY_SPACING)); //number of holes for 180degree of basket

    degree_per_hole = 180 / number_of_holes;

    for (i = [0:(number_of_holes - 1)]) {
        rotate(a = (i * degree_per_hole), v = [0, 0, 1]) union() {
            translate ([radius - CUTOUT_RADIUS_ADJUSTMENT/2, 0, height]) cube([SIDE_THICKNESS*(1 + (TOP_RADIUS-BOTTOM_RADIUS)/BASKET_HEIGHT) + 2 * CUT_DELTA, SIDE_HOLE_WIDTH, SIDE_HOLE_HEIGHT], center = true);
            translate ([-(radius - CUTOUT_RADIUS_ADJUSTMENT/2), 0, height]) cube([SIDE_THICKNESS*(1 + (TOP_RADIUS-BOTTOM_RADIUS)/BASKET_HEIGHT) + 2 * CUT_DELTA, SIDE_HOLE_WIDTH, SIDE_HOLE_HEIGHT], center = true);
        }
    }

};

module generate_base_holes(spacing_from_edge) {
    intersection (){
        translate([BOTTOM_HOLES_X_OFFSET, BOTTOM_HOLES_Y_OFFSET, (BOTTOM_THICKNESS / 2)]) intersection (){
            union () {
                for (x = [((BOTTOM_HOLE_X_SIZE + BOTTOM_HOLE_X_SPACING) / 2):(BOTTOM_HOLE_X_SPACING + BOTTOM_HOLE_X_SIZE): BOTTOM_RADIUS+BOTTOM_HOLES_X_OFFSET]) {
                    translate ([x, 0, 0]) cube([BOTTOM_HOLE_X_SIZE, 2 * BOTTOM_RADIUS, BOTTOM_THICKNESS + 2 * CUT_DELTA], center = true);
                    translate ([-x, 0, 0]) cube([BOTTOM_HOLE_X_SIZE, 2 * BOTTOM_RADIUS, BOTTOM_THICKNESS + 2 * CUT_DELTA], center = true);
                }
            }

            union () {
                for (y = [((BOTTOM_HOLE_Y_SIZE + BOTTOM_HOLE_Y_SPACING) / 2):(BOTTOM_HOLE_Y_SPACING + BOTTOM_HOLE_Y_SIZE): BOTTOM_RADIUS+BOTTOM_HOLES_Y_OFFSET]) {
                    translate ([0, y, 0]) cube([2 * BOTTOM_RADIUS, BOTTOM_HOLE_Y_SIZE, BOTTOM_THICKNESS + 2 * CUT_DELTA], center = true);
                    translate ([0, -y, 0]) cube([2 * BOTTOM_RADIUS, BOTTOM_HOLE_Y_SIZE,BOTTOM_THICKNESS + 2 * CUT_DELTA], center = true);
                }
            }
        }

        translate ([0, 0, -CUT_DELTA]) cylinder(h=BOTTOM_THICKNESS + 2 * CUT_DELTA, r = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT - spacing_from_edge, $fn= CYLINDER_FACES);
    }
}  ;

module generate_clips(radius) {

    degree_per_hole = 180 / NUMBER_OF_PAIRS_OF_CLIPS;

    union() {
        for (i = [0:(NUMBER_OF_PAIRS_OF_CLIPS - 1)]) {
            rotate(a = (i * degree_per_hole), v = [0, 0, 1]) rotate(a = 90, v = [1, 0, 0]) union() {
                translate ([radius + (2 * DETACH_BASE - 1) * OFFSET_OF_CLIP_CYLINDER, 0, 0])  cylinder(h=WIDTH_OF_CLIPS, r = (HEIGHT_OF_CLIPS + OFFSET_OF_CLIP_CYLINDER), center = true, $fn=CLIP_CYLINDER_FACES);
                translate ([-radius - (2 * DETACH_BASE - 1) * OFFSET_OF_CLIP_CYLINDER, 0, 0]) cylinder(h=WIDTH_OF_CLIPS, r = (HEIGHT_OF_CLIPS + OFFSET_OF_CLIP_CYLINDER), center = true, $fn=CLIP_CYLINDER_FACES);
            }
        }
    }

};

module generate_clips_isolated(height, radius) {

    if(DETACH_BASE == 1){

        translate ([0, 0, height]) intersection() {
            generate_clips(radius);
            isolation_height = 2 * (HEIGHT_OF_CLIPS + OFFSET_OF_CLIP_CYLINDER) * sin(acos(OFFSET_OF_CLIP_CYLINDER/(HEIGHT_OF_CLIPS + OFFSET_OF_CLIP_CYLINDER)));
            cylinder(h=isolation_height, r1 = (radius + CLIP_OVERLAP + (2 * DETACH_BASE - 1) * (isolation_height / 2)*(TOP_RADIUS - BOTTOM_RADIUS) / BASKET_HEIGHT), r2 = (radius + CLIP_OVERLAP - (2 * DETACH_BASE - 1) * (isolation_height / 2)*(TOP_RADIUS - BOTTOM_RADIUS) / BASKET_HEIGHT), center = true, $fn= CYLINDER_FACES);
        }
    }
    else {
        translate ([0, 0, height]) difference() {
            generate_clips(radius);
            isolation_height = 2 * (HEIGHT_OF_CLIPS + OFFSET_OF_CLIP_CYLINDER);
            cylinder(h=isolation_height, r1 = (radius + CLIP_OVERLAP + (2 * DETACH_BASE - 1) * (isolation_height / 2)*(TOP_RADIUS - BOTTOM_RADIUS) / BASKET_HEIGHT), r2 = (radius + CLIP_OVERLAP - (2 * DETACH_BASE - 1) * (isolation_height / 2)*(TOP_RADIUS - BOTTOM_RADIUS) / BASKET_HEIGHT), center = true, $fn= CYLINDER_FACES);
        }
    }

};

if(DETACH_BASE == 0) {
    if(ENABLE_PART != 0) {
        difference(){
            cylinder(h=RIM_HEIGHT, r = TOP_RADIUS + RIM_WIDTH, $fn= CYLINDER_FACES);
            translate ([0, 0, 0]) cylinder(h=RIM_HEIGHT, r1 = TOP_RADIUS -BOTTOM_RIM_ADJUSTMENT , r2 = TOP_RADIUS - BOTTOM_RIM_ADJUSTMENT + (RIM_HEIGHT * tan(INTERFACE_ANGLE)), $fn= CYLINDER_FACES);
        }
    }

    if(ENABLE_MAIN_BASKET != 0) {
        difference(){
            union (){
                cylinder(h=BASKET_HEIGHT, r1 = BOTTOM_RADIUS, r2= TOP_RADIUS, $fn= CYLINDER_FACES);
                translate([0, 0, BASKET_HEIGHT - RIM_HEIGHT]) cylinder(h=RIM_HEIGHT, r1 = TOP_RADIUS - BOTTOM_RIM_ADJUSTMENT, r2 = TOP_RADIUS - BOTTOM_RIM_ADJUSTMENT + (RIM_HEIGHT * tan(INTERFACE_ANGLE)), $fn= CYLINDER_FACES);
            };

            translate([0, 0, BOTTOM_THICKNESS]) cylinder (h=BASKET_HEIGHT, r1 = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT , r2 = TOP_RADIUS - CUTOUT_RADIUS_ADJUSTMENT, $fn= CYLINDER_FACES);

            number_of_rows = floor((BASKET_HEIGHT - MIN_HEIGHT_BOTTOM_HOLES - MIN_HEIGHT_TOP_HOLES) / (SIDE_HOLE_HEIGHT + SIDE_HOLE_Z_SPACING));

            for (row = [0:(SIDE_HOLE_HEIGHT + SIDE_HOLE_Z_SPACING):(number_of_rows *(SIDE_HOLE_HEIGHT + SIDE_HOLE_Z_SPACING))]) {
                h = MIN_HEIGHT_BOTTOM_HOLES + row;
                r = BOTTOM_RADIUS + (h / BASKET_HEIGHT) * (TOP_RADIUS - BOTTOM_RADIUS);
                generate_side_holes(h, r);
            }

            generate_base_holes(BOTTOM_HOLE_SPACING_FROM_EDGE);
        }
        if(NUMBER_OF_PAIRS_OF_CLIPS > 0) {
            clip_height = BASKET_HEIGHT - RIM_HEIGHT - CLIP_SPACING_FROM_INTERFACE;
            generate_clips_isolated(clip_height, (BOTTOM_RADIUS + (clip_height / BASKET_HEIGHT) * (TOP_RADIUS - BOTTOM_RADIUS)));
        }
    }
}
else {
    BOTTOM_HOLE_SPACING_FROM_EDGE = BOTTOM_THICKNESS * tan(INTERFACE_ANGLE);
    if(ENABLE_MAIN_BASKET != 0) {
        difference(){
            union (){
                cylinder(h=BASKET_HEIGHT, r2 = BOTTOM_RADIUS, r1= TOP_RADIUS, $fn= CYLINDER_FACES);
                cylinder(h=RIM_HEIGHT, r = TOP_RADIUS + RIM_WIDTH, $fn= CYLINDER_FACES);
            }

            translate([0, 0, -BOTTOM_THICKNESS]) cylinder (h=BASKET_HEIGHT, r2 = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT , r1 = TOP_RADIUS - CUTOUT_RADIUS_ADJUSTMENT, $fn= CYLINDER_FACES);

            number_of_rows = floor((BASKET_HEIGHT - MIN_HEIGHT_BOTTOM_HOLES - MIN_HEIGHT_TOP_HOLES) / (SIDE_HOLE_HEIGHT + SIDE_HOLE_Z_SPACING));

            for (row = [0:(SIDE_HOLE_HEIGHT + SIDE_HOLE_Z_SPACING):(number_of_rows *(SIDE_HOLE_HEIGHT + SIDE_HOLE_Z_SPACING))]) {
                h = BASKET_HEIGHT - MIN_HEIGHT_BOTTOM_HOLES - row;
                r = TOP_RADIUS - (h / BASKET_HEIGHT) * (TOP_RADIUS - BOTTOM_RADIUS);
                generate_side_holes(h, r);
            }

            translate ([0, 0, BASKET_HEIGHT - BOTTOM_THICKNESS]) cylinder(h=RIM_HEIGHT, r1 = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT, r2 = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT - (BOTTOM_THICKNESS * tan(INTERFACE_ANGLE)), $fn= CYLINDER_FACES);

        }
        if(NUMBER_OF_PAIRS_OF_CLIPS > 0) {
            clip_height = BASKET_HEIGHT - BOTTOM_THICKNESS - CLIP_SPACING_FROM_INTERFACE;
            generate_clips_isolated(clip_height, (TOP_RADIUS - SIDE_THICKNESS - (clip_height / BASKET_HEIGHT) * (TOP_RADIUS - BOTTOM_RADIUS)));
        }
    }

    if(ENABLE_PART != 0) {
        difference(){
            cylinder(h=BOTTOM_THICKNESS, r1 = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT, r2 = BOTTOM_RADIUS - CUTOUT_RADIUS_ADJUSTMENT - (BOTTOM_THICKNESS * tan(INTERFACE_ANGLE)), $fn= CYLINDER_FACES);
            generate_base_holes(BOTTOM_HOLE_SPACING_FROM_EDGE);
        }

    }
}

```


[User]
Hey, I need some help creating a customizable box design in OpenSCAD. I've seen some cool parametric box generators out there, but I'm having trouble making my own. I want a box with rounded edges, and I need to be able to control the overall dimensions, wall thickness, and fillet radius.

Specifically, I'm looking for a box that has these features:

*   **Overall Dimensions:** Length, Width, and Height should all be adjustable parameters.
*   **Wall Thickness:**  A parameter to control the thickness of the box walls.
*   **Rounded Edges:**  A parameter to define the radius of the rounded edges/corners.
*   **Fillet Smoothness:** I want a parameter to control how smooth the rounded corners are, probably by changing the `$fn` value.
*   **Panel Tolerance:** I want to be able to specify a small gap between the box and any panels I insert inside, using a "tolerance" parameter.
*   **PCB Feet:** I need support posts inside for a PCB. I need to be able to specify the position, length, width, height, diameter, and hole diameter for each foot. I'd like to be able to specify multiple feet.
*   **Ventilation Holes:** An option to create ventilation holes/slots along the sides of the box, including a parameter for the width of the ventilation slots.
*   **Front and Back Panels:** I need the ability to define the shapes and texts of cutouts on both front and back panels. I'll need to define the position, radius, and diameter of circular cutouts and the position, width, height and corner rounding for rectangular cutouts. I'd also like to place text on the panels, both linear and circular text.

I want to be able to control which parts of the design get exported: top shell, bottom shell, front panel, and back panel, via booleans.

I'd like the generated files to include comments, use descriptive variable names, and be organized in a way that makes sense.

I'm not using any external libraries right now, so I don't need to worry about specifying them. Could you generate the necessary OpenSCAD files for this?

[Assistant]
UltimateBox.scad:
```
/*//////////////////////////////////////////////////////////////////

    FB Aka Heartman/Hearty 2016 -
    http://heartygfx.blogspot.com -
    OpenScad Parametric Box -
    CC BY-NC 3.0 License -
    ////////////////////////////////////////////////////////////////////
    12/02/2016 - Fixed minor bug
    28/02/2016 - Added holes ventilation option
    09/03/2016 - Added PCB feet support, fixed the shell artefact on export mode.

*/////////////////////////// - Info - //////////////////////////////

// All coordinates are starting as integrated circuit pins.
// From the top view :

// CoordD <--- CoordC
// ^
// ^
// ^
// CoordA ---> CoordB

////////////////////////////////////////////////////////////////////

////////// - Paramètres de la boite - Box parameters - /////////////

/ [Box dimensions] /
// - Longueur - Length
Length = 160;
// - Largeur - Width
Width = 170;
// - Hauteur - Height
Height = 100;
// - Epaisseur - Wall thickness
Thick = 2;//[2:5]

/ [Box options] /
// - Diamètre Coin arrondi - Filet diameter
Filet = 2;//[0.1:12]
// - lissage de l'arrondi - Filet smoothness
Resolution = 50;//[1:100]
// - Tolérance - Tolerance (Panel/rails gap)
m = 0.9;
// Pieds PCB - PCB feet (x4)
PCBFeet = 1;// [0:No, 1:Yes]
// - Decorations to ventilation holes
Vent = 1;// [0:No, 1:Yes]
// - Decoration-Holes width (in mm)
Vent_width = 1.5;

/ [PCB_Feet] /
//All dimensions are from the center foot axis
//Feet Position [x,y,l,w,h,dia,hole]
PCBS = [[7,6,70,50,10,8,3],
[7,70,70,50,10,8,3]];

/ [FPANEL Holes] /
// Round Holes - [x,y,r]
FP_Holes = [[27,40,8],
[47,40,8],
[67,40,8],
[93,30,10]];
// Square Holes - [x,y,w,h,f]
FP_SHoles = [[20,20,15,10,1],
[40,20,15,10,1],
[60,20,15,10,1],
[20,50,80,30,3],
[120,20,30,60,3]];
// Rounde Text [x,y,font,size,diameter,arc,start,text]
FP_RTexts = [[93,29,"Arial Black",4,10,180,0,"1 . 2 . 3 . 4 . 5 . 6"]];
//Text [x,y,font,size,text]
FP_STexts = [[20,83,"Arial Black",4,"Digital Screen"],
[120,83,"Arial Black",4,"Level"],
[20,11,"Arial Black",6," 1 2 3"]];

/ [BPANEL Holes] /
// Round Holes - [x,y,r]
BP_Holes = [[27,40,8],
[47,40,8],
[67,40,8],
[93,30,10]];
// Square Holes - [x,y,w,h,f]
BP_SHoles = [[20,20,15,10,1],
[40,20,15,10,1],
[60,20,15,10,1],
[20,50,80,30,3],
[120,20,30,60,3]];
// Rounde Text [x,y,font,size,diameter,arc,start,text]
BP_RTexts = [[93,29,"Arial Black",4,10,180,0,"1 . 2 . 3 . 4 . 5 . 6"]];
//Text [x,y,font,size,text]
BP_STexts = [[20,83,"Arial Black",4,"Digital Screen"],
[120,83,"Arial Black",4,"Level"],
[20,11,"Arial Black",6," 1 2 3"]];

/ [STL element to export] /
//Coque haut - Top shell
TShell = 0;// [0:No, 1:Yes]
//Coque bas- Bottom shell
BShell = 1;// [0:No, 1:Yes]
//Panneau avant - Front panel
FPanL = 1;// [0:No, 1:Yes]
//Panneau arrière - Back panel
BPanL = 1;// [0:No, 1:Yes]

/ [Hidden] /
// - Couleur coque - Shell color
Couleur1 = "Orange";
// - Couleur panneaux - Panels color
Couleur2 = "OrangeRed";
// Thick X 2 - making decorations thicker if it is a vent to make sure they go through shell
Dec_Thick = Vent ? Thick2 : Thick;
// - Depth decoration
Dec_size = Vent ? Thick2 : 0.8;

/////////// - Boitier générique bord arrondis - Generic rounded box - //////////

module RoundBox($a=Length, $b=Width, $c=Height){// Cube bords arrondis
$fn=Resolution;
translate([0,Filet,Filet]){
minkowski (){
cube ([$a-(Length/2),$b-(2Filet),$c-(2Filet)], center = false);
rotate([0,90,0]){
cylinder(r=Filet,h=Length/2, center = false);
}
}
}
}// End of RoundBox Module

////////////////////////////////// - Module Coque/Shell - //////////////////////////////////

module Coque(){//Coque - Shell
Thick = Thick*2;
difference(){
difference(){//sides decoration
union(){
difference() {//soustraction de la forme centrale - Substraction Fileted box

                    difference(){//soustraction cube median - Median cube slicer
                        union() {//union
                        difference(){//Coque
                            RoundBox();
                            translate([Thick/2,Thick/2,Thick/2]){
                                    RoundBox($a=Length-Thick, $b=Width-Thick, $c=Height-Thick);
                                    }
                                    }//Fin diff Coque
                            difference(){//largeur Rails
                                 translate([Thick+m,Thick/2,Thick/2]){// Rails
                                      RoundBox($a=Length-((2*Thick)+(2*m)), $b=Width-Thick, $c=Height-(Thick*2));
                                                      }//fin Rails
                                 translate([((Thick+m/2)*1.55),Thick/2,Thick/2+0.1]){ // +0.1 added to avoid the artefact
                                      RoundBox($a=Length-((Thick*3)+2*m), $b=Width-Thick, $c=Height-Thick);
                                                }
                                            }//Fin largeur Rails
                                }//Fin union
                           translate([-Thick,-Thick,Height/2]){// Cube à soustraire
                                cube ([Length+100, Width+100, Height], center=false);
                                        }
                                  }//fin soustraction cube median - End Median cube slicer
                           translate([-Thick/2,Thick,Thick]){// Forme de soustraction centrale
                                RoundBox($a=Length+Thick, $b=Width-Thick*2, $c=Height-Thick);
                                }
                            }

            difference(){// wall fixation box legs
                union(){
                    translate([3*Thick +5,Thick,Height/2]){
                        rotate([90,0,0]){
                                $fn=6;
                                cylinder(d=16,Thick/2);
                                }
                        }

                   translate([Length-((3*Thick)+5),Thick,Height/2]){
                        rotate([90,0,0]){
                                $fn=6;
                                cylinder(d=16,Thick/2);
                                }
                        }

                    }
                        translate([4,Thick+Filet,Height/2-57]){
                         rotate([45,0,0]){
                               cube([Length,40,40]);
                              }
                       }
                       translate([0,-(Thick*1.46),Height/2]){
                            cube([Length,Thick*2,10]);
                       }
                } //Fin fixation box legs
        }

    union(){// outbox sides decorations

        for(i=[0:Thick:Length/4]){

            // Ventilation holes part code submitted by Ettie - Thanks ;)
                translate([10+i,-Dec_Thick+Dec_size,1]){
                cube([Vent_width,Dec_Thick,Height/4]);
                }
                translate([(Length-10) - i,-Dec_Thick+Dec_size,1]){
                cube([Vent_width,Dec_Thick,Height/4]);
                }
                translate([(Length-10) - i,Width-Dec_size,1]){
                cube([Vent_width,Dec_Thick,Height/4]);
                }
                translate([10+i,Width-Dec_size,1]){
                cube([Vent_width,Dec_Thick,Height/4]);
                }

                }// fin de for
           // }
            }//fin union decoration
        }//fin difference decoration

        union(){ //sides holes
            $fn=50;
            translate([3*Thick+5,20,Height/2+4]){
                rotate([90,0,0]){
                cylinder(d=2,20);
                }
            }
            translate([Length-((3*Thick)+5),20,Height/2+4]){
                rotate([90,0,0]){
                cylinder(d=2,20);
                }
            }
            translate([3*Thick+5,Width+5,Height/2-4]){
                rotate([90,0,0]){
                cylinder(d=2,20);
                }
            }
            translate([Length-((3*Thick)+5),Width+5,Height/2-4]){
                rotate([90,0,0]){
                cylinder(d=2,20);
                }
            }
        }//fin de sides holes

    }//fin de difference holes

}// fin coque

////////////////////////////// - Experiment - ///////////////////////////////////////////

/////////////////////// - Foot with base filet - /////////////////////////////
module foot(FootDia,FootHole,FootHeight){
Filet=2;
color(Couleur1)
translate([0,0,Filet-1.5])
difference(){

difference(){
        //translate ([0,0,-Thick]){
            cylinder(d=FootDia+Filet,FootHeight-Thick, $fn=100);
                    //}
                rotate_extrude($fn=100){
                        translate([(FootDia+Filet*2)/2,Filet,0]){
                                minkowski(){
                                        square(10);
                                        circle(Filet, $fn=100);
                                    }
                             }
                       }
               }
        cylinder(d=FootHole,FootHeight+1, $fn=100);
           }

}// Fin module foot

module Feet(width, length, height,fdia,fhole){
//////////////////// - PCB only visible in the preview mode - /////////////////////
translate([3*Thick+2,Thick+5,height+(Thick/2)-0.5]){

%square ([length+10,width+10]);
   translate([length/2,width/2,0.5]){
    color("Olive")
    %text("PCB", halign="center", valign="center", font="Arial black");
   }
} // Fin PCB

////////////////////////////// - 4 Feet - //////////////////////////////////////////
translate([3Thick+7,Thick+10,Thick/2]){
foot(fdia,fhole,height);
}
translate([(3Thick)+length+7,Thick+10,Thick/2]){
foot(fdia,fhole,height);
}
translate([(3Thick)+length+7,(Thick)+width+10,Thick/2]){
foot(fdia,fhole,height);
}
translate([3Thick+7,(Thick)+width+10,Thick/2]){
foot(fdia,fhole,height);
}

} // Fin du module Feet

////////////////////////////////////////////////////////////////////////
////////////////////// <- Holes Panel Manager -> ///////////////////////
////////////////////////////////////////////////////////////////////////

// <- Panel ->
module Panel(Length,Width,Thick,Filet){
scale([0.5,1,1])
minkowski(){
cube([Thick,Width-(Thick2+Filet2+m),Height-(Thick2+Filet2+m)]);
translate([0,Filet,Filet])
rotate([0,90,0])
cylinder(r=Filet,h=Thick, $fn=100);
}
}

// <- Circle hole ->
// Cx=Cylinder X position | Cy=Cylinder Y position | Cdia= Cylinder dia | Cheight=Cyl height
module CylinderHole(OnOff,Cx,Cy,Cdia){
if(OnOff==1)
translate([Cx,Cy,-1])
cylinder(d=Cdia,10, $fn=50);
}
// <- Square hole ->
// Sx=Square X position | Sy=Square Y position | Sl= Square Length | Sw=Square Width | Filet = Round corner
module SquareHole(OnOff,Sx,Sy,Sl,Sw,Filet){
if(OnOff==1)
minkowski(){
translate([Sx+Filet/2,Sy+Filet/2,-1])
cube([Sl-Filet,Sw-Filet,10]);
cylinder(d=Filet,h=10, $fn=100);
}
}

// <- Linear text panel ->
module LText(OnOff,Tx,Ty,Font,Size,Content){
if(OnOff==1)
translate([Tx,Ty,Thick+.5])
linear_extrude(height = 0.5){
text(Content, size=Size, font=Font);
}
}
// <- Circular text panel->
module CText(OnOff,Tx,Ty,Font,Size,TxtRadius,Angl,Turn,Content){
if(OnOff==1) {
Angle = -Angl / len(Content);
translate([Tx,Ty,Thick+.5])
for (i= [0:len(Content)-1] ){
rotate([0,0,i*Angle+90+Turn])
translate([0,TxtRadius,0]) {
linear_extrude(height = 0.5){
text(Content[i], font = Font, size = Size, valign ="baseline", halign ="center");
}
}
}
}
}
////////////////////// <- New module Front Panel -> //////////////////////
module FPanL(){
difference(){
color(Couleur2)
Panel(Length,Width,Thick,Filet);

rotate([90,0,90]){
    color(Couleur2){

// <- Cutting shapes from here ->
for (hole = FP_Holes) {
CylinderHole(1,hole[0],hole[1],hole[2]);
}
for (shole = FP_SHoles) {
SquareHole(1,shole[0],shole[1],shole[2],shole[3],shole[4]);
}

// <- To here ->
}
}
}

color(Couleur1){
    translate ([-.5,0,0])
    rotate([90,0,90]){

// <- Adding text from here ->
for (stext = FP_STexts) {
LText(1,stext[0],stext[1],stext[2],stext[3],stext[4]);
}
for (rtext = FP_RTexts) {
CText(1,rtext[0],rtext[1],rtext[2],rtext[3],rtext[4],rtext[5],rtext[6],rtext[7]);
}

// <- To here ->
}
}
}

////////////////////// <- New module Back Panel -> //////////////////////
module BPanL(){
difference(){
color(Couleur2)
Panel(Length,Width,Thick,Filet);

rotate([90,0,90]){
    color(Couleur2){

// <- Cutting shapes from here ->
for (hole = BP_Holes) {
CylinderHole(1,hole[0],hole[1],hole[2]);
}
for (shole = BP_SHoles) {
SquareHole(1,shole[0],shole[1],shole[2],shole[3],shole[4]);
}

// <- To here ->
}
}
}

color(Couleur1){
    translate ([-.5,0,0])
    rotate([90,0,90]){

// <- Adding text from here ->
for (stext = BP_STexts) {
LText(1,stext[0],stext[1],stext[2],stext[3],stext[4]);
}
for (rtext = BP_RTexts) {
CText(1,rtext[0],rtext[1],rtext[2],rtext[3],rtext[4],rtext[5],rtext[6],rtext[7]);
}

// <- To here ->
}
}
}

/////////////////////////// <- Main part -> /////////////////////////
if(TShell==1)
// Coque haut - Top Shell
color( Couleur1,1){
translate([0,Width,Height+0.2]){
rotate([0,180,180]){
Coque();
}
}
}

if(BShell==1)
// Coque bas - Bottom shell
color(Couleur1){
Coque();
}

// Pied support PCB - PCB feet
if (PCBFeet==1)
// Feet
for (pcb = PCBS) {
translate([pcb[0],pcb[1],0]){
Feet(pcb[3], pcb[2], pcb[4],pcb[5],pcb[6]);
//width, length, height,fdia,fhole
//x,y,l,w,h,dia,hole
}
}

// Panneau avant - Front panel <<<<<< Text and holes only on this one.
//rotate([0,-90,-90])
if(FPanL==1)
translate([Length-(Thick*2+m/2),Thick+m/2,Thick+m/2])
FPanL();

//Panneau arrière - Back panel
if(BPanL==1)
rotate(180,0,0) translate([-(Thick*2+m/2),-Width+Thick+m/2,Thick+m/2])
BPanL();

```


[User]
I need help creating an OpenSCAD design for a center finder tool, the kind you use to mark the center of a dowel or flat surface. I've seen some designs online, and I'd like to make one that's parametric so I can adjust the size easily.

Here are the basic parameters I'm thinking of:

*   **Pin Diameter (pin\_d):** The diameter of the pins that will contact the edges of the material.
*   **Pin Height (pin\_h):**  The length the pins extend beyond the base.
*   **Pin Gap (pin\_gap):**  The distance between the two pins, which determines the maximum width of material the tool can handle.
*   **Base Width (base\_w):** The overall width of the tool's base.
*   **Base Thickness (base\_h):**  The thickness of the base.
*  **Wall Thickness (wall\_w):**  The thickness of the walls that make up the frame and cross braces.
*   **Pencil Hole Top Diameter (hole\_d\_top):** The diameter of the top of the hole for the marking pencil.
*   **Pencil Hole Bottom Diameter (hole\_d\_bottom):** The diameter of the bottom of the hole for the marking pencil, so it's a bit tapered.

The design should have:

*   Two pins, spaced by `pin_gap`, that protrude from the base.
*   A frame that connects the two pins. I'd like this frame to be a kind of flattened rounded rectangle with the base cutout, not just a flat rectangle.
*   Cross-"beams" or braces to add strength to the frame. These should connect the frame edges to the center section where the pencil hole is, and I think a cross shape would be good.
*   A tapered hole in the center for a pencil, with the diameters specified above.

I need to ensure the design is robust so that we don't have a super thin base area between the cutout and wall.

Could you help me create the OpenSCAD files for this?

[Assistant]
centerfind.scad:
```
/*
 *  By K. Hampf <k.hampf@gmail.com> 2021-08-31
 *  Released under the Creative Commons - Attribution license.
 *
 *  Based on https://www.thingiverse.com/thing:4745000 by "diega" which is_bool
 *  in turn a remix of https://www.thingiverse.com/thing:2199356 by "TikiLuke"
 */

/* [Pin] */

// Pin diameter
pin_d = 6;

// Pin length protruding from plate
pin_h = 10;

// Pin gap
pin_gap = 70;

/* [Base] */

// Base width
base_w = 15;

// Base thickness
base_h = 3;

// Wall thickness
wall_w = 2;

/* [Hole] */

// Pencil hole top diameter
hole_d_top = 3.5;

// Pencil hole bottom diameter
hole_d_bottom = 2.5;

/* [Hidden] */

assert(wall_w * 2 <= base_w);
assert(pin_d <= base_w);
assert(hole_d_bottom <= hole_d_bottom);
assert(hole_d_bottom + 2 * wall_w <= base_w);

fudge = 0.001; // To avoid rounding errors
$fn=100; // facets

cross_l = sqrt(pow(pin_gap,2)+pow(wall_w,2));
cross_w = base_w-2*wall_w;
cross_a = atan(cross_w/cross_l);

// To-do
// Assertions

// Do it
frame();
difference() {
    crossbars();
    penhole();
}
pins();

module frame() {
    // Frame
    difference() {
        hull() {
            translate ([-(pin_gap-base_w+pin_d)/2,0,0])
                cylinder(d=base_w,h=base_h);
            translate ([(pin_gap-base_w+pin_d)/2,0,0])
                cylinder(d=base_w,h=base_h);
        }
        /*
        // Square cutout
        color("red") translate ([-pin_gap/2,-base_w/2+wall_w,1])
            cube([pin_gap,base_w-2*wall_w,base_h]);
        */
        // Rounded cutout
        hull() {
            translate([-pin_gap/2+cross_w/2,0,-fudge])
                cylinder(d=base_w-2*wall_w,h=base_h+2*fudge);
            translate([pin_gap/2-cross_w/2,0,-fudge])
                cylinder(d=base_w-2*wall_w,h=base_h+2*fudge);
        }
    }
}

module crossbars() {
    /*translate([-pin_gap/2,-wall_w/2,0])
        cube([pin_gap,wall_w,base_h]);
    */
    translate([-wall_w/2,-base_w/2,0])
        cube([wall_w,base_w,base_h]);
    rotate(a=cross_a)
        translate([-cross_l/2,-wall_w/2,0])
            cube([cross_l,wall_w,base_h]);
    rotate(a=-cross_a)
        translate([-cross_l/2,-wall_w/2,0])
            cube([cross_l,wall_w,base_h]);
    cylinder(h=base_h, d1=hole_d_top+wall_w, d2=hole_d_bottom+wall_w);
}

module pins() {
    translate ([-pin_gap/2,0,0]) cylinder(d=pin_d, h=base_h+pin_h);
    translate ([pin_gap/2,0,0]) cylinder(d=pin_d, h=base_h+pin_h);
}

module penhole() {
    translate([0,0,-fudge])
        cylinder(h=base_h+2*fudge,d1=hole_d_top,d2=hole_d_bottom);
}
```


[User]
I'd like to design a customizable square sundial, with a separate base and gnomon for easy assembly. I'm thinking something along the lines of a design where the base is a flat square with a depressed center and a slot for the gnomon, and the gnomon itself is a separate piece designed to fit snugly into that slot. I'd like the design to be parametric so I can adjust for different latitudes, longitudes, and time zones, as well as the size of the dial itself.

Here are some specific design parameters I have in mind:

*   **Print Options:** A boolean to switch between generating the gnomon or the base.
*   **Location Text:** A string to print along the bottom edge of the dial, for example, "My City".
*   **Location:** Latitude and longitude, in decimal degrees. Make sure to handle negative longitudes for the Western Hemisphere.
*   **Time Zone:** Time offset from UTC.
*   **Dial Size:** Horizontal size of the dial, a single number since it's square.
*   **Dial Height:** Height of the dial base.
*   **Dial Border:** Width of the border around the dial.
*   **Center Depression:** Depth of the depressed center area on the dial.
*   **Location Text Size:** Size of the location text on the dial (not in mm).
*   **Location Text Height:** Height of the location text (mm).
*   **Location Text Offset:** Distance of the location text from the dial edge (mm).
*   **Gnomon Width:** Thickness of the gnomon (mm).
*   **Gnomon Depth:** How deep the gnomon fits into the base (mm).
*   **Gnomon Tolerance:** Tolerance for fitting the gnomon into the base (mm).
*   **Gnomon Vertical Offset:** Offset from the dial border to start gnomon (mm).

The sundial should be marked with hour lines, calculated based on the location and time zone. I also want the hour markings to be printed on the border of the dial, using either 12 or 24 hour format (maybe a parameter to switch between those?). The calculations should take into account the time zone and longitudinal offset. The hour lines need to intersect with the edge of the dial. For the text, I think I want to use the 'Letters.dxf' file. It does not need to be scaled, the size parameter will handle it.

So, basically, I'm looking for an OpenSCAD file (or files) that will generate the sundial base and gnomon separately, all configurable by these parameters. I will need some kind of text library (like the 'WriteText.scad') that can do letters on the dial.

[Assistant]
SquareDial.scad:
```
/*
	SquareDial.scad
	Customizable Square SunDial

	Created by Gary Stebbins
*/

use <WriteText.scad>

// True (checked) to print gnomon, False (unchecked) to print dial base
printGnomon = false;

// Text to be printed on bottom border of dial
locationName = "Four Corners";
// Latitude in decimal degrees - positive for North (design not tested for south)
latitude = 	  36.9990861;	// Latitude in decimal degrees
// Longitude in decimal degrees - Negative for West!
longitude = -109.0457702;	// Longitude in decimal degrees. Mind the sign if it's negative!

// Time Offset from UTC. West is negative. Pacific Standard Timezone is -8.
timeZone = -7;

// All measurements are in mm
// Horizontal size of dial (length of side)
dialSide = 125;
// Height of dial base
dialHeight = 8;
// Width of dial border
dialBorder = 10;
// Center area depression
dialDepth = 2;

// Text information
// size of location text (not in mm)
locationTextSize = 5;
// height of location text
locationTextHeight = 1;
// location text distance from dial edge (mm)
locationTextOffset = 2;

// Thickness of gnomon (mm)
gnomonWidth = 5;
// How deep gnomon is set into the base (mm)
gnomonDepth = 4;
// Tolerance for fitting gnomon into base (varies by printer and material)
gnomonTolerance = 0.2;
// Offset from dialBorder to start gnomon
gnomonVOffset = 15;

/********************************************************************
  End of User Parameters
/********************************************************************/

timeCorrection = -longitude + (timeZone*15.0);	// Offset from center of timezone
dialInside = dialSide-(2*dialBorder);			// inset area of dial
gnomonHOffset = (dialInside-gnomonWidth)/2;
gnomonBaseLength = dialInside-gnomonVOffset;
gnomonBaseLeft  = [(dialSide-gnomonWidth)/2, dialBorder+gnomonVOffset, dialHeight-dialDepth];
gnomonBaseRight = [(dialSide+gnomonWidth)/2, dialBorder+gnomonVOffset, dialHeight-dialDepth];

dialUpperAngle = atan(((dialInside-gnomonWidth)/2)/gnomonBaseLength);
dialLowerAngle = atan(gnomonVOffset/((dialInside-gnomonWidth)/2))+90;
//echo("dialUpper, dialLower angles",dialUpperAngle,dialLowerAngle);

/* Hours of the day to be printed around border */
legendArray= ["0","1","2","3","4","5","6","7","8","9","10","11","12","1","2","3","4","5","6","7","8","9","10","11"];

/* Enable the following statement for 24 hour markings */
//legendArray= ["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23"];

/************************************************************
	Create main dial
************************************************************/
module SquareDial() {
	difference() {
		union() {
			difference() {
				cube([dialSide, dialSide, dialHeight]);	// Full dial
				// remove center area
				translate([dialBorder,dialBorder,dialHeight-dialDepth]) cube([dialInside, dialInside, dialHeight]);
				// remove gnomon slot
				translate([(dialSide/2)-(gnomonWidth/2)-(gnomonTolerance/2), dialBorder+gnomonVOffset-gnomonTolerance, dialHeight-dialDepth-gnomonDepth])
					cube([gnomonWidth+gnomonTolerance, gnomonBaseLength+gnomonTolerance, gnomonDepth]);
			}

			// Write location information at bottom of dial
			translate([dialSide/2, locationTextSize/2+locationTextOffset, dialHeight+(locationTextHeight/2)]) {
				write(locationName, center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
			}
			// Step through all hours and create hour lines and hour legend
			// Change this if you want a different range of hours on your sundial
			for (hour = [4:20]){	// hours from 4am to 8pm
				noonDegrees = (hour - 12) * 15;	// degrees from noon; am negative, pm positive
				correctedDegrees = noonDegrees - timeCorrection;	// correct for offset from center of timezone
				// rawLineAngle is angle of line, but when |correctDegrees| > 90, result is off by 180°
				// switch is correction factor for this case
				rawLineAngle = atan(tan(correctedDegrees) * sin(latitude));
				switch = sign(correctedDegrees) * 180.0 * sign(abs(sign(correctedDegrees)-sign(rawLineAngle)));
				lineAngle = -(rawLineAngle + switch);
				// Debugging statement...
				//echo("hour,adjusted time,rawLineAngle,switch,lineAngle", hour, correctedDegrees, rawLineAngle, switch, lineAngle);
				aa = abs(lineAngle);

				// Figure which quarter of each side the hour line is in
				// and calculate line length to intersect with side of dial.
				// If to the left of the gnomon, offset from lower left corner of gnomon,
				// if to the right, offset from lower right corner to compensate for gnomon width.
				// Also insert hour legend in dial border at appropriate position.
				if (aa >= dialLowerAngle) // lower part of dial
				{
					aangle = 180-aa;
					lineLength = gnomonVOffset/cos(180-aa);
					if (correctedDegrees<0)
						translate(gnomonBaseLeft) rotate(lineAngle) drawLine(lineLength);
					else
						translate(gnomonBaseRight) rotate(lineAngle) drawLine(lineLength);
				}
				else
				{
					if (aa >= 90)		// lower middle part of dial
					{
						aangle = aa-90;
						lineLength = gnomonHOffset/cos(aa-90);
						legendHOffset = gnomonHOffset+(dialBorder/2);
						legendVOffset = (gnomonHOffset+(dialBorder/2))*tan(aangle);
						// write legend
						if (correctedDegrees<0)	// left side of dial
						{
							translate(gnomonBaseLeft) rotate(lineAngle) drawLine(lineLength);
							translate([dialBorder/2, dialBorder+gnomonVOffset-legendVOffset, dialHeight+(locationTextHeight/2)])
								write(legendArray[hour], center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
						}
						else					// right side of dial
						{
							translate(gnomonBaseRight) rotate(lineAngle) drawLine(lineLength);
							translate([dialSide-(dialBorder/2), dialBorder+gnomonVOffset-legendVOffset, dialHeight+(locationTextHeight/2)])
								write(legendArray[hour], center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
						}
					}
					else
					{
						if (aa >= dialUpperAngle) // upper middle part of dial
						{
							aangle = 90-aa;
							lineLength = gnomonHOffset/cos(90-aa);
							legendHOffset = gnomonHOffset+(dialBorder/2);
							legendVOffset = (gnomonHOffset+(dialBorder/2))*tan(aangle);

							if (correctedDegrees<0)
							{
								translate(gnomonBaseLeft) rotate(lineAngle) drawLine(lineLength);
								translate([dialBorder/2, dialBorder+gnomonVOffset+legendVOffset, dialHeight+(locationTextHeight/2)])
								write(legendArray[hour], center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
							}
							else
							{
								translate(gnomonBaseRight) rotate(lineAngle) drawLine(lineLength);
								translate([dialSide-(dialBorder/2), dialBorder+gnomonVOffset+legendVOffset, dialHeight+(locationTextHeight/2)])
								write(legendArray[hour], center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
							}

						}
						else	// upper
						{
							aangle=aa;
							lineLength = gnomonBaseLength/cos(aa);
							legendHOffset = gnomonBaseLength*tan(aangle);
							legendVOffset = dialSide-(dialBorder/2);

							if (correctedDegrees<0)
							{
								translate(gnomonBaseLeft) rotate(lineAngle) drawLine(lineLength);
								translate([gnomonHOffset+dialBorder-legendHOffset, legendVOffset, dialHeight+(locationTextHeight/2)])
								write(legendArray[hour], center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
							}
							else
							{
								translate(gnomonBaseRight) rotate(lineAngle) drawLine(lineLength);
								translate([gnomonHOffset+dialBorder+gnomonWidth+legendHOffset, legendVOffset, dialHeight+(locationTextHeight/2)])
								write(legendArray[hour], center=true, h=locationTextSize, t=locationTextHeight, bold=0, font="Letters.dxf");
							}
						}
					}
				}
			} // for hour
		} // union
		// subtract out gnomon slot
		translate([(dialSide/2)-(gnomonWidth/2)-(gnomonTolerance/2), dialBorder+gnomonVOffset-gnomonTolerance, dialHeight-dialDepth-gnomonDepth])
			cube([gnomonWidth+gnomonTolerance, gnomonBaseLength+gnomonTolerance, gnomonDepth+dialHeight]);

	} // difference
} // module SquareDial

/************************************************************
	gnomon() creates the gnomon to fit into the square sundial
************************************************************/
module Gnomon(){

	difference() {
		translate([0,-gnomonDepth,0]) cube([gnomonBaseLength, gnomonBaseLength+gnomonDepth, gnomonWidth]); // Full gnomon
// does this work over the full range?
        //subtract linear portion above gnomon
		rotate([0,0,latitude]) translate([0,0,-1]) cube([gnomonBaseLength*4, gnomonBaseLength*4, gnomonWidth+2]);
	}
}

/************************************************************
	drawLine() creates a line of the specified length
	parameters:
		lineLength: length of line
		lineWidth:	width of line
		lineHeight:	height of line
************************************************************/
module drawLine(lineLength, lineWidth=1, lineHeight=1) {
	translate([-lineWidth/2, 0, 0]) cube([lineWidth, lineLength, lineHeight]);
}

if (printGnomon)
	Gnomon();
else
	SquareDial();

```
WriteText.scad:
```
//module write(word){
// parameters:
		//text		text to write
		//center	true to center text [x,y,z], false to lock lower left corner to [0,0,0]
		//h			text character height
		//t			text thickness
		//font		font file to use
		//bold		bold amount
module write(word, center=true, h, t, font="Letters.dxf", bold){
	//echo (h);
	//echo (word);
	//echo ("There are " ,len(word) ," letters in this string");
//	echo ("The second letter is ",word[1]);
//	echo (str(word[0],"_"));
	space = 1;
	minkowski() {
		rotate(a=0,[0,0,-1]){

			for (r = [0:len(word)]){   // count off each character
				// if the letter is lower case, add an underscore to the end for file lookup
				if ((word[r] == "a" ) || (word[r]== "b")  || (word[r]== "c")
				  || (word[r]== "d") || (word[r]== "e") || (word[r]== "f")
				  || (word[r]== "g") || (word[r]== "h")  || (word[r]== "i")
					  || (word[r]== "j") || (word[r]== "k") || (word[r]== "l")
					  || (word[r]== "m") || (word[r]== "n") || (word[r]== "o")
					  || (word[r]== "p") || (word[r]== "q") || (word[r]== "r")
				  || (word[r]== "s") || (word[r]== "t") || (word[r]== "u")
					  || (word[r]== "v") || (word[r]== "w") || (word[r]== "x")
					  || (word[r]== "y" )|| (word[r]== "z")){
					if (center == true)  {
						translate([0,-h/2,0]){
							scale([.125*h,.125*h,t]){
								translate([ (-len(word)*5.5*space/2) + (r*5.5*space),0,0])
								//offset(delta = 20, join_type = "round") {
								linear_extrude(height=1,convexity=10,center=true){
									import(file = font,layer=str(word[r],"_"));
								}//}
							}
						}
					}else{
						translate([0,0,t/2]){
							scale([.125*h,.125*h,t]){
								translate([r*5.5*space,0,0])
								//offset(delta = 20, join_type = "round") {
								linear_extrude(height=1,convexity=10,center=true){
									import(file = font,layer=str(word[r],"_"));
								}//}
							}
						}
					}

				}else{
					if (center == true)  {
						translate([0,-h/2,0]){
							scale([.125*h,.125*h,t]){
								translate([ (-len(word)*5.5*space/2) + (r*5.5*space),0,0])
								//offset(delta = 20, join_type = "round") {
								linear_extrude(height=1,convexity=10,center=true){
									import(file = font,layer=str(word[r]));
								}//}
							}
						}
					}else{
						translate([0,0,t/2]){
							scale([.125*h,.125*h,t]){
								translate([r*5.5*space,0,0])
								//offset(delta = 20, join_type = "round") {
								linear_extrude(height=1,convexity=10,center=true){
									import(file = font,layer=str(word[r]));
								}//}
							}
						}
					}
				}
			}
		} // rotate
	cube([bold*.1*h,bold*.1*h,.00001]);
	} // minkowski
} // module write

```