OFEP8: OpenSCAD Style Guide for the OpenFlexure Project

OFEP 8
Author Julian Stirling
Created 27-April-2026
Status Approved
Type of OFEP Development
Approved date 29-April-2026
Requires implementation No
Implemented date N/A
Updated dates (post-approval) N/A

1. Introduction

This OFEP provides style guidelines and coding conventions for OpenSCAD code in the OpenFlexure project. The coding conventions are fairly strict, with a focus on readability and reliability. It is more important that someone coming to a code block can understand it, than it is that for a code block to be fast to write initially. The style guide forbids some features of OpenSCAD as they can cause unexpected behaviour.

The most important principle is readability and low cognitive complexity. Cognitive complexity is a measurement of how difficult it is for a human to understand a block of code. Reducing cognitive complexity can be achieved with small modules that have one task, simple lines of code that do one thing, and splitting up calculations to clearly-named variables.

2. Line length

Lines should be limited in length, with 100 characters being the maximum line length where possible.

We note however that arbitrarily splitting lines (as is common with autoformatted code) often produces significantly more confusing code than a simple refactoring. In the case of this long line:

a_long_module_name(size=[size_of_gap.x, size_of_gap.x+22*(2*n+1)+2.54*0.1, size_of_gap.z-2.54*0.1]);

It is not really improved in clarity by:

a_long_module_name(
    size=[size_of_gap.x, size_of_gap.x+22*(2*n+1)+2.54*0.1, size_of_gap.z-2.54*0.1]);

or

a_long_module_name(size=[size_of_gap.x,
                         size_of_gap.x+22*(2*n+1)+2.54*0.1,
                         size_of_gap.z-2.54*0.1]);

Instead a small refactor will improve clarity significantly:

clearance = 2.54*0.1;
y_spacing = 22*(2*n+1);
size = size_of_gap + [0, y_spacing+clearance, -clearance];
a_long_module_name(size=size);

3. Use of libraries

The OpenFlexure project is very large and hence is divided into many individual .scad files. We separate files into:

  • Geometry files - which produce a specific STL. Ideally these should have the following structure:

    • A docstring.
    • An SPDX identifier (see Section 7.2).
    • All use imports
    • Any global variables for customising the geometry from command line
    • A single line calling a module.
    • Optionally: a single module definition. This should be a simple module the calls a library. It can be used or lay out parts, orient parts, or calculate input parameters.
  • Library files - which define functions and modules that may be needed by other libraries and geometry files. These should create no geometry. Ideally these should have the following structure:

    • A docstring.
    • An SPDX identifier (see Section 7.2).
    • All use imports.
    • Multiple module and/or function definitions.

Library files should always be called with the use command and never with the include command. For example:

use <./libs/utilities.scad>

rather than

include <./libs/utilities.scad>

include effectively writes the content of one file into another. This provides access to the included modules not only in this library, but also in any other called by this library, or any other that this library calls. This can lead to a very cluttered namespace.

Calling with include is used by some projects to allow one file to override the global variables of the library file. This, however, causes unwanted and hard to debug effects when multiple files include that same library. The global variable will only be modified for one file causing inconsistent results. Disallowing includes forces all parameters affecting library functions and modules to be passed as arguments, which improves code readability.

It is considered best practice to use relative paths beginning with with ./ to ensure that local paths are used in case that other libraries of the same name are on the PATH. There is also no need to add a final semicolon to the end of a use line.

use <./libs/utilities.scad>

rather than

use <libs/utilities.scad>

or with an unneeded semicolon:

use <./libs/utilities.scad>;

4. Global variables

Global variables should be avoided except in geometry files when an option needs to be accessible from the command line with the -D flag. Limit these options with a preference to creating extra geometry files rather than providing multiple global options.

NOTE: The -D command line flag overwrites a given global variable in all files whether called via use or include. For this reason:

  • Global variables must not be used in library files
  • Geometry files must not be called as library files
  • If it happens that a module or function that has been placed in a geometry file is needed elsewhere, then that module must be moved to a new or existing library file.

To make global variables explicit we use uppercase naming style. Underscores are allowed but should not be the first or last character. A minimum of 3 characters is required. For example:

TALL = true;

rather than

tall = true;

or

T = true;

4.1. Strategies for avoiding global variables

Global variables in OpenSCAD can be used for two main purposes. Firstly to provide globally accessible configuration options. Secondly to provide globally accessible constants. For clarity on what is considered constant this OpenFlexure style treats these cases separately.

4.1.1. Configuration dictionaries

For configuration options we use a dictionary structure that consists of lists, of 2-element lists. Each 2-element list represents a key value pair. The library libdict.scad provides numerous functions to validate and interact with these dictionaries. The dictionary can then be passed through to all modules and functions that need it. This provides clarity of which modules and functions are affected by configuration options, and also allows the same module to be run with two different options from within the same file. See OFEP1 for more detail.

An example dictionary for configuration options:

rms_f50d13_config = [["optics_type", "RMS"],
                     ["camera_type", camera_type],
                     ["tube_lens_ffd", 47],
                     ["tube_lens_f", 50],
                     ["tube_lens_r", 12.7/2+0.1],
                     ["objective_parfocal_distance", 45],
                     ["beamsplitter", beamsplitter],
                     ["gripper_t", 1],
                     ["tube_length", 150],
                     ["camera_mount_top_z", dt_bottom() - 3 - 8],
                     ["camera_rotation", 0],
                     ["beamsplitter_rotation", 0]];

4.1.2. Globally accessible constants as functions

For globally accessible constants the OpenFlexure style requires the use of functions. While this does clutter calculations that use these constants with extra brackets, it has two added benefits. Firstly, it makes all constants read only. Secondly, it allows constants to be imported into a file from a library using use rather than include (See Section 3.).

function leg_link_spacing() = 10;

5. Naming conventions

For consistency with OpenSCAD built-ins. Both functions and modules use snake_case naming, as do variables. This does limit the ability to distinguish between modules and functions by naming convention.

As previously mentioned, global variables use upper case naming, as underscores are allowed this is sometimes called SCREAMING_SNAKE_CASE.

5.1. variable naming

Descriptive variable names are preferred over single and double letter variable names. With the exception of module arguments or module-scope variables where that module has a clear diameter, radius, or height. In which case using variables such as d, d1, d2, r, r1, r2, h is allowed as it provides consistency with built-in OpenSCAD modules.

Variables are only accessible within the scope which they are defined. As such it is only important that the name makes sense within the context of the scope (i.e. the module or function), but not necessarily within the context of the whole project.

For example the following variable name is too verbose as it is repeating the name of the module

module important_widget(){
    important_widget_corner_radius = 10;
    ...

a more appropriate name might be

module important_widget(){
    corner_radius = 10;
    ...

or even

module important_widget(){
    corner_r = 10;
    ...

5.2. Module and function naming

As modules and functions imported into an OpenSCAD file via use (or include, but that is not permitted in this style guide, see section 3) are not namespaced it is important that module and function names are very explicit. For example within a library called important_widget.scad the following module name would be insufficient:

module mounting_holes(){
    ...

Instead a more explicit module name would be required:

module important_widget_mounting_holes(){
    ...

5.3. Argument naming

When naming arguments for functions and modules it is best to consider the context where the module will be used to ensure that the name makes sense in this context.

Take this example:

module dumbell(d, length){
    reflect_z(){
        translate_z(length/2 - tiny()){
            cylinder(h=25, d=d, center=true);
        }
    }
    cylinder(h=length, d=25, center=true);
}

It may appear clear here when writing the fucntion that d is the plate diameter, and that the length is the grip length. But once the function call is seen in isolation it is significantly less clear:

dumbell(d=100, length=150);

Instead more explicit naming is helpful:

module dumbell(plate_d, grip_length){
    reflect_z(){
        translate_z(grip_length/2 - tiny()){
            cylinder(h=25, d=plate_d, center=true);
        }
    }
    cylinder(h=grip_length, d=25, center=true);
}

When called this is significantly clearer:

dumbell(plate_d=100, grip_length=150);

Names should be long enough to be clear, without being so long that module calls become hard to parse. As described above with variables, standard openscad variables such as d, r and h can be used as they have a clear defined meaning in the language. Otherwise it is best to refrain from single letter names.

6. Modules and Functions

Modules and functions are the primary tools for reducing cognitive complexity in OpenSCAD code. They should be written to do one clearly defined task, and to make the design intent obvious to a reader unfamiliar with the implementation.

6.1. Keep modules and functions short

Modules and functions should be short enough that their purpose can be understood at a glance. If a module or function:

  • requires extensive scrolling to read in full, or
  • contains many nested scopes, or
  • repeats the same logic or calculations, or
  • performs multiple logically distinct tasks

then it should be split into smaller units.

Short modules are easier to name, document, test, and reuse correctly.

6.2. One responsibility per module or function

Each module or function should have a single responsibility.

  • Functions should perform one calculation and return a value.
  • Modules should create one geometric feature or transformation.

6.3. Minimise argument list length

Large argument lists increase cognitive complexity, particularly when modules are called far from their definition or when positional arguments are used.

Prefer the following strategies:

  • Use keyword arguments rather than positional arguments.
  • Group related parameters into configuration dictionaries.
  • Split large modules into smaller modules with simpler interfaces.

Don't:

widget(20, 15, 3, true, false, 0.2, 7);

Do:

widget(size=[20, 15, 3],
       include_mounts=true,
       include_chamfers=false,
       clearance=0.2,
       screw_length=7);

Note that it is not possible to enforce that arguments are keyword-only when writing a module so this will need to be enforced for each module call.

6.4. Explicit undef rather than -1 or 0

If an argument has a default value that needs to be calculated from other input values, then this should be explicitly identified using undef as the default value. Avoid ambiguous placeholders such as 0 or -1.

Use the helper function if_undefined_set_default to replace undef. As OpenSCAD doesn't allow variables to be updated, create a new name.

Don't:

module box(width, depth=-1, height=-1){
    size = [width, depth>0?depth:width, height>0?height:width];
    ...

Do:

module box(width, depth=undef, height=undef){
    box_depth = if_undefined_set_default(depth, width);
    box_height = if_undefined_set_default(height, width);
    size = [width, box_depth, box_height];
    ...

If the variable to be used is calculated from the input (when supplied) use the built in is_undef:

Do:

module hole_from_bottom(r, h, base_w=undef, delta_z=0.5, layers=4, big_bottom=true){
    base = is_undef(base_w) ? [2*r, 2*r, tiny()] : [base_w, 2*r, 2*delta_z];
    ...

rather than

Don't:

module hole_from_bottom(r, h, base_w=-1, delta_z=0.5, layers=4, big_bottom=true){
    base = base_w>0 ? [base_w,2*r,2*delta_z] : [2*r,2*r,tiny()];
    ...

7. Docstrings and comments

7.1. Docstrings

Docstrings are comments that describe what specific files, modules and functions do. The OpenFlexure codebase has experimented with different conventions in the past. There are no Docstring standards for OpenSCAD. Some projects provide documentation conventions, but due to stability issues and the complexity of some of these conventions these existing forms were not used.

The OpenFlexure Project uses SCA2D as a linter for OpenSCAD. SCA2D has been developed further to generate documentation, it has its own conventions loosely based on Python docstrings.

7.1.1. Docstring conventions

The conventions are:

  • Use /* */-style C comments.
  • Do not include leading *s on each subsequent line.
  • The first line has a 1 sentence summary in the imperative mood.
  • Document parameters using Sphinx-style directives:
    • :param arg1: Description of the argument
    • :return: or :returns: to describe return values (used for functions only)
    • :assert condition: to describe assertions or expected conditions (in place of :raises:)
  • Multiline descriptions (for :param ...:, :return:, or :assert ...) should indent subsequent lines by 4 spaces.
  • All other content is treated as Markdown.
  • Use fenced code blocks (```) for examples or inline code.

For example:

/* Create a box with optional bevelled edges.

    :param size: A 2D vector specifying the width and height
    :param bevel: Optional bevel radius (default 0)
*/
module box(size, bevel = 0) {
    ...

or the following with an example that will show an image in the rendered documentation:

/* Create a printable horizontal hole with a sloped overhang.

Generates a horizontal cylindrical hole with a top block that forms a
45 degree printable slope. This shape is designed to be subtracted from
solids to create holes that print cleanly without support.

:param h: length of the hole along its axis  
:param r: radius of the cylindrical hole  
:param center: whether to centre the hole on its axis  
:param extra_height: extra height for the printable roof above the hole

### Example
```example
difference(){
    cube([20, 10, 10], center=true);
    translate([0, 0, 0]){
        printable_horizontal_hole(h=20, r=3);
    }
}
```
*/

7.1.2. Rendering images for examples

To include a rendered image of an example use a fenced code block that begins with ```example. This blocks trigger automatic image generation as part of the documentation build.

Customisation is currently fairly limited.

  • To set the camera angle use the special scad variables ($vpt, $vpr, and $vpd) in your example.
  • By default preview mode is used. To use render mode add the word render after example: ```example render
  • By default images have the axes showing, add no-axes after example to suppress rendering axes: ```example no-axes

7.2. Copyright statements

Rather than specifying full names of authors (which goes out of date) and full names of licenses, use the machine readable SPDX convention, followed by a statment about where to find the authorship information. This should be using single line // comments so it is not interpreted as a docstring.

Don't:

* ...                                                             *
*                                                                 *
* (c) Aaron A. Aaronson, January 2025                             *
* Released under the CERN Open Hardware License                   *
*                                                                 *
******************************************************************/

Do:

// SPDX-License-Identifier: CERN-OHL-S-2.0
// For copyright and authorship information, see the Git history at:
// https://gitlab.com/openflexure/openflexure-microscope

7.3. Comments describing a specific variable, expression, or module call

Comments should be written on their own line rather than at the end of a code line. This reduces the tendency to create very long lines. It also makes it clearer whether the comment or the code itself has changed between commits.

  • Comments describing a specific variable, expression, or module call go on the line preceding the code it is describing

Don't:

clearance = 0.1*inch; //clearance is 1/10 of an inch

Do:

// clearance is 1/10 of an inch
clearance = 0.1*inch;

One notable exception to the above rules is that when defining a dictionary, end of line comments can be used to give information on the keys in the dictionary. For example:

Do:

my_dict = [["leg_r", 30], // radius of the leg
           ["sample_z", 75 ], // z position of sample
           ["foot_height", 15] //the height of the feet
          ];
  • Comments describing conditional flow (i.e. what happens withing the scope of an if or else statement) should be the first line of that scope. This provides consistency for the if and the else scopes.

Do:

// Calculate the fancy factor using the input parameter
fancy_factor = fancy_parameter_to_factor(fancy_parameter);
if (forward){
    //Translate forward (+x) by the fancy parameter before rotating
    translate([fancy_factor, 0, 0]){
        rotate([0, 0, 17]){
            children();
        }
    }
}
else {
    // If backwards, rotate first, then translate in -x
    rotate([0, 0, 17]){
        translate([-fancy_factor, 0, 0]){
            children();
        }
    }
}

8. Spacing

Spacing is important both for readability and to make code more uniform, which makes it more simple to refactor.

8.1. Trailing whitespace and lines

Trailing whitespace on a line should be avoided. At the end of a file there should be exactly one empty line. Multiple empty lines is excessive, however if there is no new line at the end of a file then adding anything extra to the file will cause git to identify a change to an unchanged line of code (the last line is changed by adding a new line character).

8.2. Spacing out sections of code

Blank lines within modules or other code blocks can be used to break up logically separate sections. However, the preference is to break the code itself up into small modules and functions.

There should be two clear lines between each module definition and between each function definition.

8.3. Variable assignment

Variable assignment should have one space either side of the equality

variable = 17;

8.4. Keyword argument

Keyword argument assignment should have no spaces around the equality

cube(cube_size, center=true);

8.5. List elements and function arguments

List elements and function arguments should have a single space after the comma.

cube_size = [1, 2, 3];
cube(cube_size, center=true);

See Section 9.1. for indenting long lists or argument lists that require multiple lines

8.6. Ternaries

Ternaries should have one space either side of the ? and the :

translate_z(shift ? -tiny() : 0){
    ...

Nested ternaries are also often unavoidable in openscad. Long or nested ternaries should be split over multiple lines and indented (see Section 9.6.).

8.7. Binary operators

Use spacing around operators sparingly to highlight lower priority operations

variable = 2*r + 2*w;

rather than

variable = 2 * r + 2 * w;

Also always have equal space either side of an operator

variable = 2*r+ 2*w;

9. Indentation and braces.

The OpenFlexure style guide favours:

  1. Four spaces per indentation
  2. Hanging indents aligned with the first item for long function calls and long list definitions
  3. Using only one module per line
  4. Explicitly defining all scopes with braces
  5. Breaking long calculations into multiple stages
  6. Four space indentation for any new scope
  7. Hanging indentation for list and argument indentation (See Sections 9.1 and 9.2)

Don't:

translate([20+cos(30)*50,sin(30)*50,h-2])rotate([0,0,90+extra_rot])cube([30+2*(wall+clearance),20+2*(wall+clearance),plate_t]);

Do:

walls_and_clearance = 2*(wall+clearance);
space_needed = [30, 20, plate_t];
plate_size = space_needed + [1, 1, 0]*walls_and_clearance;

arm_angle = 30;
arm_offset = [50*cos(arm_angle), 50*sin(arm_angle)];
plate_pos = [space_needed.y+arm_offset.x, arm_offset.y, h-plate_t];
translate(plate_pos){
    rotate_z(90+extra_rot){
        cube(plate_size);
    }
}

9.1. List and dictionary definition

Lists are generally written on one line unless the lines get to long. In general indentation should be hanging to improve readability. In the case of multidimensional arrays it is useful to break the line at each dimension.

For example in many cases the following:

Do:

hole_positions = [[10, 20],
                  [20, 20],
                  [30, 20],
                  [40, 20],
                  [50, 20],
                  [10, 50],
                  [20, 50],
                  [30, 50],
                  [40, 20],
                  [50, 20]];

is clearer than

Don't:

hole_positions = [[10, 20], [20, 20], [30, 20], [40, 20], [50, 20], [10, 50], [20, 50], [30, 50], [40, 50], [50, 50]];

However, for a much smaller list a single line would be appropriate:

Do:

hole_positions = [[10, 20], [20, 20]];

We don't use a 4-space indentation for list continuation to separate control flow indenation (4 spaces) from line coninuation (aligned with first element).

Don't:

hole_positions = [
    [10, 20],
    [20, 20],
    [30, 20],
    [40, 20],
    [50, 20],
    [10, 50],
    [20, 50],
    [30, 50],
    [40, 20],
    [50, 20]
];

In the case of dictionaries hanging indentation should be aligned with the first item:

Do:

rms_f50d13_config = [["optics_type", "RMS"],
                     ["camera_type", camera_type],
                     ["tube_lens_ffd", 47],
                     ["tube_lens_f", 50],
                     ["tube_lens_r", 12.7/2+0.1],
                     ["objective_parfocal_distance", 45],
                     ["beamsplitter", beamsplitter],
                     ["gripper_t", 1],
                     ["tube_length", 150],
                     ["camera_mount_top_z", dt_bottom() - 3 - 8],
                     ["camera_rotation", 0],
                     ["beamsplitter_rotation", 0]];

rather than:

Don't:

rms_f50d13_config = [["optics_type", "RMS"],
    ["camera_type", camera_type],
    ["tube_lens_ffd", 47],
    ["tube_lens_f", 50],
    ["tube_lens_r", 12.7/2+0.1],
    ["objective_parfocal_distance", 45],
    ["beamsplitter", beamsplitter],
    ["gripper_t", 1],
    ["tube_length", 150],
    ["camera_mount_top_z", dt_bottom() - 3 - 8],
    ["camera_rotation", 0],
    ["beamsplitter_rotation", 0]];

9.2. Module Use (without Children)

When using modules, minimise excessive calculations within arguments. Long calculations often have commas, making it hard to see how many arguments are used. It is best practice to break up the calculation into meaningful variables that describe the design intent as well as the physical shape.

Don't:

cube([17*2+0.1*25.4*2+35, 17*2+0.1*25.4*2+75, 17*2+0.1*25.4*2+45]);

Do:

wall_thickness = 17;
inch = 25.4;
//clearance is 1/10 of an inch
clearance = 0.1*inch;
inner_dimensions = [35, 75, 45];
extra_size = (wall_thickness+clearance) * [2, 2, 2];
cube(inner_dimensions + extra_size); 

When a module call uses a large number of arguments this can increase the line length well beyond an ideal line length, especially when the module call is already indented. In this case hanging indentation should align with the first argument. For example:

Do:

fancy_widget(size=[20, 20, 10],
             clearance=1,
             shaft_length=22,
             include_chamfers=true,
             include_mounting_holes=true);

rather than leaving on one line.

Don't:

fancy_widget(size=[20, 20, 10], clearance=1, shaft_length=22, include_chamfers=true, include_mounting_holes=true);

or indenting the arguments by only one 4-space tab:

Don't:

fancy_widget(
    size=[20, 20, 10],
    clearance=1,
    shaft_length=22,
    include_chamfers=true,
    include_mounting_holes=true
);

9.3. Flow Control including Module Use with Children

When performing multiple transformations each transform should be on a new line with it's scope explicitly defined with braces. By keeping one module per line, each transformation becomes clear at a glance. This is especially true when module arguments contain functions.

Don't:

rotate_x(asin(width/(arm_length+widget_length))+extra_rotation)rotate_z(180)rotate_x(90)translate([sin(-h/2),cos(-h/2),0])cube(cube_size);

Do:

rotate_x(asin(width / (arm_length+widget_length)) + extra_rotation){
    rotate_z(180){
        rotate_x(90){
            translate([-h/2, -h/2, 0]){
                cube(cube_size);
            }
        }
    }
}

One problem that can be caused by splitting transformations onto their own line is the Pyramid of Doom, where text gets very indented. The best way to avoid the Pyramid of Doom is to split code into smaller functions. If code is indented in 6-7 indents this means that a reader needs to keep track of these 6-7 transformations. Splitting this into a named function reduces the cognitive complexity of the code. The above example can be further improved as:

x_rot = asin(width / (arm_length+widget_length)) + extra_rotation;
euler_xzx_rotation(90, 180, x_rot){
    translate([-h/2, -h/2, 0]){
        cube(cube_size);
    }
}

Where the a module for the Euler rotation to replace the three rotations would need to be defined (with appropriate docstrings).

Note: Explicit scope definition is easy for a linter to enforce and it prevents bugs occurring when semicolons are missed. For example, consider the code:

//This code has a bug!!
widget(widget_size)
translate([0, 0, 10])
    another_widget(bigger_size);

A missing semi-colon after widget causes another_widget to be a child of widget. This can cause it to disappear from the geometry. By enforcing the style:

//This code has a bug!!
widget(widget_size)
translate([0, 0, 10]){
    another_widget(bigger_size);
}

The linter will warn that widget has an implicit scope. This will alert the coder to add a semi-colon after widget as another_widget should not be its child. Flow control statements such as for and if should also be on their own lines and should use explicit scopes.

Don't:

for (hole=hole_positions) translate(hole){
    cylinder(r=3, h=7);
}

Do:

for (hole=hole_positions){
    translate(hole){
        cylinder(r=3, h=7);
    }
}

And in the case of statements with else cases:

Don't:

if (rounded){cylinder(r=3, h=7, center=true);}else{cube([3, 3, 7], center=true);}

Nor:

if (rounded){cylinder(r=3, h=7, center=true);}
else{cube([3, 3, 7], center=true);}

Do:

if (rounded){
    cylinder(r=3, h=7, center=true);
}
else{
    cube([3, 3, 7], center=true);
}

9.4. Module Definition

  • There should be two blank lines before the docstring.
  • The module should be directly preceded by its docstring.
  • The module definition should be on one line if there is space within 100 characters.
  • If the module definition needs to be split over multiple lines hanging indentation should be used.
  • There should be no spaces between arguments and default values, but one space (or newline) after each comma.
  • There should be no space between the ) and { when defining the module.
  • The module code should be 4 spaced indented.
  • The closing } should be on its own line.
    ...
}


/* Translate geometry along the x-axis.

Equivalent to `translate([dist, 0, 0])`.

:param x_tr: Distance to translate in the x-direction
*/
module translate_x(x_tr){
    translate([x_tr, 0, 0]){
        children();
    }
}

9.5. Function Definition

  • There should be two blank lines before the docstring.
  • The function should be directly preceded by its docstring.
  • The function definition should be on one line if there is space within 100 characters.
  • If the function definition needs to be split over multiple lines hanging indentation should be used.
  • There should be no spaces between arguments and default values, but one space after each comma.
  • There should be single spaces either side of the =
  • Ideally the entire function should be on one line if short enough.
  • Functions that need significant calculation should use let blocks to allow variables to be set.
    • Ideally one variable per line in the let block.
    • When using other complex functionality such a list comprehension, do your best to indent long lines in a way that provides context
    • The final return should be on its own line with the ) that closes the let.
    ...
}


/* The dimensions of the nut slot in the actuator.

:return: A vector giving the space for the nut including clearance:
    `[space across flats, space across corners, space for height]`
*/
function actuator_nut_slot_size() = let(
    //maximum width of the m3 nut (flat to flat) specified by ISO 4032
    nut_w = 5.5,
    // Multiplying by a clearance factor of 1.091 that has been tested empirically
    // for many years, to provide good grip on the nuts when in the top of the
    // trap which is 90% of this slot size.
    nut_w_clear = nut_w*1.091,
    // ISO 4032 specifies a maximum height for the m3 nut of of 2.4mm
    nut_h = 2.4
) [nut_w_clear, nut_w_clear/sin(60), nut_h+0.6];

9.6. Ternaries

Ideally ternaries should be short and on one line. If the ternary becomes long it can be split over three lines, with results indented:

width = include_clearance ?
    outer_width - 2*wall_thickness - 2*clearance :
    outer_width - 2*wall_thickness;

alternatively the calculation can be reduced by assigning a clearly named variable:

inner_width = outer_width - 2*wall_thickness;
width = include_clearance ? inner_width - 2*clearance : inner_width;

Nested ternaries are often unavoidable in OpenSCAD as variables do not outlive their scope, and hence cannot be calculated within if-else statements. In such a case, try to arrange the logic so the next ternary is on the second (else) result. Each ternary should be:

position = in_place ?
    assembled_position :
    for_printing ?
        print_position :
        home_position;

If the logic is so complex that nested ternaries are needed for both results, or if ternaries nesting becomes deeper than double nesting the code should be refactored in one of the following ways:

  • Define functions to perform certain ternary operations with clear naming.
  • Use dictionaries to define possible return options
  • Refactor the code to perform multiple single ternary operations and then combine the values later.