Rendering these values on the server. This takes a few seconds.
Download STL
Read this model as text

The module library behind the Gridfinity generators: the grid constants, the base profile, bin bodies, walls, tabs, cutouts, the baseplate, screw and magnet holes, and the geometry helpers they share.

This version defines modules for other models to call. It renders no solid, so there is no shape to describe.

MITNo geometry

Gridfinity core modules

remixcad/gridfinity-core@1.0.0

Remix this
Report

For a legal or policy problem: a license violation, personal data, somebody else’s work republished as their own. A model that prints badly is a comment, not a report.

Sign in to report

0reused0 remixes · 0 imports0downloads1.0.0latest

Import this version

Nothing to change here. The source declares no parameter with a range, so there is nothing a slider could move.

use <remixcad/gridfinity-core@1.0.0/src/core/standard.scad>

Description

What is this

The module library behind the Gridfinity generators: the grid constants, the base profile, bin bodies, walls, tabs, cutouts, the baseplate, screw and magnet holes, and the geometry helpers they share.

What you use it for

Import it when you are building something that has to sit on a Gridfinity grid, or that has to accept one. The generators published alongside it do exactly that, and are the shorter route if you just want a bin.

Design notes

Gridfinity heights are counted in 7 mm units. A bin is 7 times its unit height in millimetres, with a 4.4 mm stacking lip added on top when the lip is enabled, so the stock unit heights of 2, 3 and 6 come out at 18.4, 25.4 and 46.4 mm.

Upstream carries its own copy of the threading library. Here that is an import instead, so there is one copy and the reuse is counted where it happens.

This model defines modules and produces no shape of its own, so it has no preview and no STL.

Parameters

Nothing to set here. The constants live in src/core/standard.scad and the generators expose what is worth changing.

Use with your agent
{
  "mcpServers": {
    "remixcad": {
      "type": "http",
      "url": "https://remix-cad.vercel.app/api/mcp"
    }
  }
}
get_model("remixcad/gridfinity-core@1.0.0")

Returns the manifest, the source, the import closure and the license obligations. Full MCP setup.

Parameter table

Parsed from the Customizer annotations in src/core/standard.scad
Parameters of Gridfinity core modules, with the ranges the source declares
NameDefaultRangeUnitDescription
d_wall0.95
r_f22.8
d_div1.2
GRID_DIMENSIONS_MM[42, 42]
TOLLERANCE0.02
LAYER_HEIGHT0.2
MAGNET_HEIGHT2
d_hole_from_side8
HOLE_DISTANCE_FROM_BOTTOM_EDGE4.8
REFINED_HOLE_BOTTOM_LAYERS2
MAGNET_HOLE_CRUSH_RIB_COUNT8
CHAMFER_ADDITIONAL_RADIUS0.8
CHAMFER_ANGLE45
BASEPLATE_SCREW_COUNTERBORE_HEIGHT3
TAB_WIDTH_NOMINAL42
_tab_depth15.85
_tab_support_angle36
_tab_support_height1.2
STACKING_LIP_FILLET_RADIUS0.6
STACKING_LIP_SUPPORT_HEIGHT1.2
_stacking_lip_support_angle45
BASE_TOP_DIMENSIONS[41.5, 41.5]
BASE_HEIGHT7
BASE_THUMBSCREW_OUTER_DIAMETER15
BASE_THUMBSCREW_PITCH1.5
bp_h_bot6.4
bp_cut_size21.4
bp_cut_depth4
bp_rcut_width8.5
bp_rcut_length4.25
bp_rcut_depth2
r_skel2
h_skel1

Lineage

4 models depend on this one

Only what this model came from and what came from it.

Remix, the code changed Import, used unchanged and pinned The version you are on Withdrawn, still recorded

License

Whoever downloads this takes these obligations along
  • remixcad/gridfinity-core@1.0.0 is MIT
    • @File gridfinity-baseplate.scad @Brief Functions and standards to make a SINGLE gridfinity baseplate. @Copyright Arthur Moore 2025 MIT License @WARNING: Where possible, `use` thise file. Constants may change between minor versions. /
  • remixcad/threads@1.0.0 is CC0

Source

src/core/standard.scad is the entrypoint
src/core/base.scad
/**
 * @file
 * @brief Functions to make one or more Gridfinty base(s).
 */

include <standard.scad>
use <gridfinity-rebuilt-holes.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/grid.scad>
use <../helpers/list.scad>
use <../helpers/shapes.scad>
use <remixcad/threads/threads.scad>

_debug = false;
//$fa = 8;
//$fs = 0.25;

/**
 * @brief Create the base of a gridfinity bin, or use it for a custom object.
 * @param grid_size Number of bases in each dimension. [x, y]
 * @param grid_dimensions [length, width] of a single Gridfinity base.
 * @param thumbscrew Enable "gridfinity-refined" thumbscrew hole in the center of each base unit. This is a ISO Metric Profile, 15.0mm size, M15x1.5 designation.
 */
module gridfinityBase(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, hole_options=bundle_hole_options(), only_corners=false, thumbscrew=false) {
    assert(is_list(grid_dimensions) && len(grid_dimensions) == 2 &&
        grid_dimensions.x > 0 && grid_dimensions.y > 0);
    assert(is_list(grid_size) && len(grid_size) == 2 &&
        grid_size.x > 0 && grid_size.y > 0);
    assert(
        is_bool(only_corners) &&
        is_bool(thumbscrew)
    );

    individual_base_size_mm = grid_dimensions - BASE_GAP_MM;

    // Final size of the base top. In mm.
    // Subtracting BASE_GAP_MM to remove the perimeter overhang.
    grid_size_mm = [
        grid_dimensions.x * grid_size.x,
        grid_dimensions.y * grid_size.y
    ] - BASE_GAP_MM;

    // Top which ties all bases together
    _base_bridge_solid(grid_size_mm);

    if(only_corners) {
        difference(){
            pattern_grid(grid_size, grid_dimensions, true, true) {
                base_solid(individual_base_size_mm);
            }

            if(thumbscrew) {
                thumbscrew_position = grid_size_mm - individual_base_size_mm;
                pattern_grid([2, 2], thumbscrew_position, true, true) {
                    _base_thumbscrew();
                }
            }

            _base_holes(hole_options, grid_size_mm);
            _base_preview_fix();
        }
    }
    else {
        pattern_grid(grid_size, grid_dimensions, true, true)
        block_base(hole_options, individual_base_size_mm, thumbscrew=thumbscrew);
    }
}

/**
 * @brief Create the base of a gridfinity bin, or use it for a custom object.
 * @param grid_size Size in number of bases. [x, y]
 * @param grid_dimensions [length, width] of a single Gridfinity base.
 * @param wall_thickness How thick the walls, and holes (if enabled) are.
 * @param bottom_thickness Height of the solid bottom.
 * @param hole_options @see block_base_hole.hole_options
 * @param only_corners Only put holes on each corner.
 */
module gridfinity_base_lite(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, wall_thickness, bottom_thickness, hole_options=bundle_hole_options(), only_corners = false) {
    assert(is_list(grid_size) && len(grid_size) == 2 && grid_size.x > 0 && grid_size.y > 0);
    assert(is_num(wall_thickness) && wall_thickness > 0);
    assert(is_num(bottom_thickness)
        && bottom_thickness >= 0
        && bottom_thickness <= BASE_HEIGHT);
    assert(is_bool(only_corners));

    wall_thickness_2d = [wall_thickness, wall_thickness];

    solid_bridge_height = bottom_thickness - BASE_PROFILE_HEIGHT;
    profile_height = min(bottom_thickness, BASE_PROFILE_HEIGHT);

    individual_base_size_mm = grid_dimensions - BASE_GAP_MM;
    // Final size of the base top. In mm.
    // Subtracting BASE_GAP_MM to remove the perimeter overhang.
    grid_size_mm = [
        grid_dimensions.x * grid_size.x,
        grid_dimensions.y * grid_size.y
    ] - BASE_GAP_MM;

    //Hollow bridging structure to tie the bases together
    color("RosyBrown")
    difference() {
        _base_bridge_solid(grid_size_mm);

        //Creates a square bridging structure.
        translate([0, 0, BASE_PROFILE_HEIGHT-TOLLERANCE])
        pattern_grid(grid_size, grid_dimensions, true, true)
        linear_extrude(BASE_BRIDGE_HEIGHT+2*TOLLERANCE)
        rounded_square(
            individual_base_size_mm - 2 * wall_thickness_2d,
            BASE_TOP_RADIUS-wall_thickness,
            center=true);

        // Chamfer the inner edges
        translate([0, 0, BASE_PROFILE_HEIGHT-TOLLERANCE])
        intersection() {
            pattern_grid(grid_size, grid_dimensions, true, true)
            _lite_bridge_chamfer(
                individual_base_size_mm,
                wall_thickness);

            // Don't touch the exterior.
            linear_extrude(BASE_BRIDGE_HEIGHT+2*TOLLERANCE)
            rounded_square(
                grid_size_mm- 2 * wall_thickness_2d,
                BASE_TOP_RADIUS-wall_thickness,
                center=true);
        }
    }

    //Solid bridging structure
    if (solid_bridge_height > 0) {
        _base_bridge_solid(grid_size_mm, solid_bridge_height);
    }

    render()
    if(only_corners) {
        difference() {
            union() {
                pattern_grid(grid_size, grid_dimensions, true, true)
                base_outer_shell(wall_thickness, profile_height, individual_base_size_mm);
                _base_holes(hole_options, grid_size_mm, -2*wall_thickness);
            }

            _base_holes(hole_options, grid_size_mm);
            _base_preview_fix();
        }
    }
    else {
        pattern_grid(grid_size, grid_dimensions, true, true) {
            difference() {
                union() {
                    base_outer_shell(wall_thickness, profile_height, individual_base_size_mm);
                    _base_holes(hole_options, individual_base_size_mm, -2*wall_thickness);
                }
                _base_holes(hole_options, individual_base_size_mm);
                _base_preview_fix();
            }
        }
    }
}

/**
 * @brief The solid bridging structure for bases.
 * @details Already translated to the correct height. Per the standard.
 * @param grid_size_mm Total grid size.
 *        Taking BASE_GAP_MM into account.
 * @param height
 */
module _base_bridge_solid(grid_size_mm, height=BASE_BRIDGE_HEIGHT) {
    assert(is_valid_2d(grid_size_mm)
        && is_positive(grid_size_mm));
    assert(is_num(height)
        && height >= 0
        && height <= BASE_BRIDGE_HEIGHT);

    color("RosyBrown")
    translate([0, 0, BASE_PROFILE_HEIGHT])
    linear_extrude(height)
    rounded_square(grid_size_mm, BASE_TOP_RADIUS, center=true);
}

/**
 * @brief Negative to chamfer the bridge structure for lite bases.
 * @param individual_base_size_mm Size of a single base.
 *        Taking BASE_GAP_MM into account.
 * @param wall_thickness
 */
module _lite_bridge_chamfer(individual_base_size_mm, wall_thickness) {
    assert(is_valid_2d(individual_base_size_mm)
        && is_positive(individual_base_size_mm));
    assert(is_num(wall_thickness)
        && wall_thickness > 0);

    chamfer_polygon = [
        [0, 0], // Inside of bin
        [wall_thickness, BASE_BRIDGE_HEIGHT],
        [0, BASE_BRIDGE_HEIGHT],
    ];
    translated_polygon = foreach_add(
        chamfer_polygon, [
            BASE_TOP_RADIUS-wall_thickness-TOLLERANCE,
            2*TOLLERANCE
        ]);
    sweep_inner = individual_base_size_mm
        - 2 * [BASE_TOP_RADIUS, BASE_TOP_RADIUS];

    sweep_rounded(sweep_inner)
    polygon(translated_polygon);
}

/**
 * @brief Solid polygon of a gridfinity base.
 * @details Ready for use with `sweep_rounded(...)`.
 *          Square internals allow for `cube` to fill the center.
 */
module _base_polygon() {
    translated_line = foreach_add(BASE_PROFILE, [BASE_BOTTOM_RADIUS, 0]);
    solid_profile = concat(translated_line,
        [
            [0, BASE_PROFILE_HEIGHT],  // Go in to form a solid polygon
            [0, 0],  // Needed since start has been translated.
        ]
    );
    polygon(solid_profile);
}

/**
 * @brief A single solid Gridfinity base.
 * @details Height is BASE_PROFILE_HEIGHT.
 * @warning Does not include the structure tying the bases together.
 * @param top_dimensions [x, y] size of a single base.  Only set if deviating from the standard!
 */
module base_solid(top_dimensions=BASE_TOP_DIMENSIONS) {
    assert(is_valid_2d(top_dimensions)
           && min(top_dimensions) > 2 * BASE_TOP_RADIUS,
        str("Minimum size of a single base must be greater than ", 2 * BASE_TOP_RADIUS)
    );

    base_bottom = base_bottom_dimensions(top_dimensions);
    sweep_inner = foreach_add(base_bottom, -2*BASE_BOTTOM_RADIUS);
    cube_size = foreach_add(base_bottom, -BASE_BOTTOM_RADIUS);

    union(){
        sweep_rounded(sweep_inner)
        _base_polygon();

        //Inner fill
        translate([0, 0, BASE_PROFILE_HEIGHT/2])
        cube([cube_size.x, cube_size.y, BASE_PROFILE_HEIGHT], center=true);
    }
}

/**
 * @brief Internal function to create the negative for a Gridfinity Refined thumbscrew hole.
 * @details Magic constants are what the threads.ScrewHole function does.
 */
module _base_thumbscrew() {
    ScrewThread(
        1.01 * BASE_THUMBSCREW_OUTER_DIAMETER + 1.25 * 0.4,
        BASE_PROFILE_HEIGHT,
        BASE_THUMBSCREW_PITCH
    );
}

/**
 * @brief Internal Code. Generates the 4 holes for a single base.
 * @details Need this fancy code to support refined holes and non-square bases.
 * @param top_dimensions [length, width] of a single Gridfinity base.
 * @param hole_options @see bundle_hole_options
 * @param offset @see block_base_hole.offset
 */
module _base_holes(hole_options, top_dimensions=BASE_TOP_DIMENSIONS, offset=0) {
    hole_position = foreach_add(
        base_bottom_dimensions(top_dimensions)/2,
        -HOLE_DISTANCE_FROM_BOTTOM_EDGE
    );

    for(a=[0:90:270]){
        // i and j represent the 4 quadrants.
        // The +1 is used to keep any values from being exactly 0.
        j = sign(sin(a+1));
        i = sign(cos(a+1));
        translate([i * hole_position.x, j * hole_position.y, 0])
        rotate([0, 0, a])
        block_base_hole(hole_options, offset);
    }
}

/**
 * @brief A single Gridfinity base.  With holes (if set).
 * @details Height is BASE_PROFILE_HEIGHT.
 * @param hole_options @see block_base_hole.hole_options
 * @param top_dimensions [x, y] size of a single base.  Only set if deviating from the standard!
 * @param thumbscrew Enable "gridfinity-refined" thumbscrew hole in the center of each base unit. This is a ISO Metric Profile, 15.0mm size, M15x1.5 designation.
 */
module block_base(hole_options, top_dimensions=BASE_TOP_DIMENSIONS, thumbscrew=false) {
    assert(is_valid_2d(top_dimensions) && is_positive(top_dimensions));
    assert(is_bool(thumbscrew));

    base_bottom = base_bottom_dimensions(top_dimensions);

    difference() {
        base_solid(top_dimensions);

        if (thumbscrew) {
            _base_thumbscrew();
        }
        _base_holes(hole_options, top_dimensions);
        _base_preview_fix();
    }
}

/**
 * @brief Outer shell of a Gridfinity base.
 * @details Height is BASE_PROFILE_HEIGHT.
 * @param wall_thickness How thick the walls are.
 *                       Capped at BASE_TOP_RADIUS.
 * @param bottom_thickness How thick the bottom is.
 * @param top_dimensions [x, y] size of a single base.  Only set if deviating from the standard!
 * @IMPORTANT: This is highly optimized to reduce the amount of geometry generated.
 */
module base_outer_shell(wall_thickness, bottom_thickness, top_dimensions=BASE_TOP_DIMENSIONS) {
    assert(is_num(wall_thickness) && wall_thickness > 0);
    assert(is_num(bottom_thickness)
        && bottom_thickness >= 0
        && bottom_thickness <= BASE_PROFILE_HEIGHT);
    assert(is_valid_2d(top_dimensions)
           && min(top_dimensions) > 2 * BASE_TOP_RADIUS,
        str("Minimum size of a single base must be greater than ", 2 * BASE_TOP_RADIUS)
    );

    base_bottom = base_bottom_dimensions(top_dimensions);
    sweep_inner = foreach_add(base_bottom, -2*BASE_BOTTOM_RADIUS);
    cube_size = foreach_add(base_bottom, -BASE_BOTTOM_RADIUS);

    optimized_wall = wall_thickness <= BASE_BOTTOM_RADIUS;
    optimized_bottom = optimized_wall
        || bottom_thickness < (wall_thickness - BASE_BOTTOM_RADIUS);

    if(_debug)
        echo(
            optimized_wall=optimized_wall,
            optimized_bottom=optimized_bottom
        );

    union(){
        //Sides
        if(optimized_wall) {
            sweep_rounded(sweep_inner)
            _base_polygon_lite(wall_thickness, bottom_thickness);
        } else {
            sweep_rounded(sweep_inner)
            difference() {
                _base_polygon();

                translate([-wall_thickness, 0, 0])
                _base_polygon();
            }
        }

        //Bottom
        if (bottom_thickness > 0)
        if(optimized_bottom){
            translate([0, 0, bottom_thickness/2])
            cube(concat(cube_size, bottom_thickness), center=true);
        } else {
            intersection() {
                translate([0, 0, bottom_thickness/2])
                cube(concat(top_dimensions, bottom_thickness), center=true);
                base_solid(top_dimensions=top_dimensions);
            }
        }
    }
}

/**
 * @brief Optimized lite version of `_base_polygon`.
 * @details Ready for use with `sweep_rounded(...)`.
 *          Produces a shape with a specific thickness.
 *          Bottom internals are squared for use with `cube`.
 * @param wall_thickness How thick the resulting walls are.
 *        Max allowed is BASE_BOTTOM_RADIUS.
 * @param bottom_thickness How thick the bottom is.
 *        Max allowed is BASE_PROFILE_HEIGHT.
 * @warning: Walls may be slightly thicker than expected as they slope to the appropriate bottom_thickness.
 */
module _base_polygon_lite(wall_thickness, bottom_thickness) {
    assert(is_num(wall_thickness)
        && wall_thickness > 0
        && wall_thickness <= BASE_BOTTOM_RADIUS);
    assert(is_num(bottom_thickness)
        && bottom_thickness >= 0
        && bottom_thickness <= BASE_PROFILE_HEIGHT);

    translated_line = foreach_add(BASE_PROFILE, [BASE_BOTTOM_RADIUS, 0]);
    inner_line = reverse(
        foreach_add(translated_line, [-wall_thickness, 0])
    );

    first_point = inner_line[0];
    last_point = inner_line[len(inner_line) -1];

    // Ensures the end is squared off.
    capped_inner_line = [
        for(p = inner_line)
            [p.x, max(p.y, bottom_thickness)]
    ];

    solid_profile = concat(
        translated_line,
        capped_inner_line,
        [last_point] // Go back to start.
    );
    polygon(solid_profile);
}

/**
 * @brief Internal code.  Fix base preview rendering issues.
 * @details Preview does not like perfect top/bottoms.
 */
module _base_preview_fix() {
    if($preview){
        cube([10000, 10000, 0.01], center=true);
        translate([0, 0, BASE_PROFILE_HEIGHT])
        cube([10000, 10000, 0.01], center=true);
    }
}
src/core/bin.scad
/**
 * @file
 * @brief Functions to create a Gridfinty bin, and cutouts in said bin.
 * @details Uses a struct like syntax to avoid globals.
 * @example ```
 *     bin1 = new_bin(...)
 *     bin_render(bin1) {
 *       bin_subdivide(bin1, [x_divisions, y_divisions]) {
 *         ...
 *       }
 *     }
 * ```
 */

include <standard.scad>
use <base.scad>
use <gridfinity-rebuilt-holes.scad>
use <wall.scad>
use <../helpers/grid.scad>
use <../helpers/grid_element.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/list.scad>
use <../helpers/shapes.scad>

/*
 * @brief Initialize A Gridfinity Bin
 * @details Creates the top portion of a bin, and sets some gloal variables.
 * @param grid_size Size in number of bases. [x, y]
 * @param height_mm Bin height in mm.
 *                  Excludes STACKING_LIP_HEIGHT.
 *                  Includes BASE_HEIGHT.
 * @param fill_height_mm Height of the solid which fills a bin.  In mm.
          Set to 0 for automatic.
          Negative numbers are subtracted from the height.
          Set to -height_mm for no infill.
 * @param include_lip If the bin should have a stacking lip.
 * @param hole_options @see bundle_hole_options
 * @param only_corners If only the outer corners of the bin should have holes.
 * @param thumbscrew If the bin's base should have a thumbscrew hole.
 * @param grid_dimensions [length, width] of a single Gridfinity base.
 * @param base_thickness Lower this to create a "lite" bin with a hollow base.
 */
function new_bin(
        grid_size,
        height_mm,
        fill_height = 0,
        include_lip = true,
        hole_options=bundle_hole_options(),
        only_corners=false,
        thumbscrew=false,
        grid_dimensions = GRID_DIMENSIONS_MM,
        base_thickness = BASE_HEIGHT
    ) =
    assert(is_valid_2d(grid_size) && is_positive(grid_size))
    assert(is_num(height_mm) && height_mm >= BASE_HEIGHT)
    assert(is_num(fill_height))
    assert(is_bool(include_lip))
    assert(is_hole_options(hole_options))
    assert(is_bool(only_corners))
    assert(is_bool(thumbscrew))
    assert(is_valid_2d(grid_dimensions) && is_positive(grid_dimensions))
    assert(is_num(base_thickness)
        && base_thickness >= 0
        && base_thickness <= BASE_HEIGHT)
    assert(thumbscrew == false
        || base_thickness == BASE_HEIGHT,
        "Thumscrews are not compatible with custom base_thickness.")
    assert(!include_lip
        || fill_height <= 0
        || fill_height <= height_mm - STACKING_LIP_SUPPORT_HEIGHT,
        str("Maximum fill_height for this bin is", height_mm - STACKING_LIP_SUPPORT_HEIGHT))
    let(fill_height_calculated = include_lip ?
        height_mm - BASE_HEIGHT - STACKING_LIP_SUPPORT_HEIGHT
        : height_mm - BASE_HEIGHT)
    let(fill_height_real =
        fill_height > 0 ? fill_height
        : fill_height_calculated + fill_height
    )
    // Subtracting BASE_GAP_MM to remove the perimeter overhang.
    let(base_grid=new_grid(
        concat(as_2d(grid_size), 1),
        concat(as_2d(grid_dimensions), height_mm),
        true,
        _BIN_BASE_GAP_MM_PERIMETER)
    )
    [
        "gridfinity_bin_struct",
        base_grid,
        base_thickness,
        fill_height_real,
        include_lip,
        undef,
        undef,
        undef,
        hole_options,
        only_corners,
        thumbscrew
    ];

/*
 * @brief Render a solid bin.
 * @details Any children are subtracted from the bin.
 *          Automatically translated so [0, 0, 0] is the center of the bin, at the top of the fill.
 *     Stacking lip guards are always in place.
 * @warning Stacking lip guards may be removed in the future.
 * @param bin The bin to render.  Created by `new_bin`.
 * @param enable_guards Prevent infill from cutting into the base.
 *     In general, ajust `new_bin(base_thickness=...)` instead.
 */
module bin_render(bin, enable_guards=true) {
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    );
    assert(is_bool(enable_guards));
    fill_height = max(bin[3], 0);
    include_lip = bin[4];

    // If no lip is present, the outer wall is handled by the infill.
    if (include_lip) {
        bin_render_wall(bin);
    }

    // While not required, lite bases will not show correctly in preview without this.
    render()
    difference() {
        union() {
            bin_render_infill(bin);
            if(!enable_guards)
                bin_render_base(bin);
        }
        translate([0, 0, BASE_HEIGHT + fill_height + TOLLERANCE])
        children();
    }
    if(enable_guards)
        bin_render_base(bin);
}

/**
 * @brief Render the inner solid part of a bin.
 * @details The resulting solid intersects with the walls.
 *          Allowing a bin without a stacking lip to have a wall.
 * @param bin The bin to render.  Created by `new_bin`.
 */
module bin_render_infill(bin) {
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    );
    base_grid = bin[1];
    base_thickness = bin[2];
    fill_height = bin[3];  //May be negative.
    hole_options = bin[8];
    only_corners = bin[9];
    thumbscrew = bin[10];

    grid_size_mm = as_2d(grid_get_total_dimensions(base_grid));
    grid_size = bin_get_bases(bin);
    grid_dimensions = bin_get_grid_dimensions(bin);

    color("firebrick")
    translate([0, 0, BASE_HEIGHT])
    linear_extrude(max(fill_height, 0))
    rounded_square(
        grid_size_mm - [TOLLERANCE, TOLLERANCE],
        BASE_TOP_RADIUS,
        center=true);

    // "lite" bins can use the base as infill.
    if(base_thickness != BASE_HEIGHT) {
        total_fill_height = bin_get_infill_size_mm(bin).z;
        intersection() {
            translate([0, 0, base_thickness])
            linear_extrude(total_fill_height)
            square(grid_size_mm, center=true);

            color("firebrick")
            gridfinityBase(grid_size,
                grid_dimensions=grid_dimensions,
                hole_options=hole_options,
                only_corners=only_corners,
                thumbscrew=thumbscrew);
        }
    }
}

/**
 * @brief Render the wall of a bin.  With a stacking lip.
 * @param bin The bin to render.  Created by `new_bin`.
 * @see bin_render_infill if you do not want a stacking lip.
 */
module bin_render_wall(bin) {
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    );
    let(base_grid = bin[1])
    let(wall_height = grid_get_total_dimensions(base_grid)
        - [0, 0, BASE_HEIGHT])

    translate([0, 0, BASE_HEIGHT])
    render_wall(wall_height);
}

/**
 * @brief Render the base of a bin.
 * @param bin The bin to render.  Created by `new_bin`.
 */
module bin_render_base(bin) {
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    );
    base_thickness = bin[2];
    hole_options = bin[8];
    only_corners = bin[9];
    thumbscrew = bin[10];

    grid_size = bin_get_bases(bin);
    grid_dimensions = bin_get_grid_dimensions(bin);

    if(base_thickness == BASE_HEIGHT) {
        gridfinityBase(grid_size,
            grid_dimensions=grid_dimensions,
            hole_options=hole_options,
            only_corners=only_corners,
            thumbscrew=thumbscrew);
    } else {
        gridfinity_base_lite(grid_size,
            grid_dimensions=grid_dimensions,
            wall_thickness=d_wall,
            bottom_thickness=base_thickness,
            hole_options=hole_options,
            only_corners=only_corners);
    }
}

/**
 * @brief Subdivide the bin.  Any children will be cut out from the center of each sub-division.
 * @details Supports `grid_element_current`.
 *          Takes wall thickness into account.
 *          Meaning `bin_subdivide(bin, bin_bin_get_bases(bin))` does **not** exactly correspond to the center of each base!
 *          Does nothing if there is no infill.
 * @param bin A bin created by the `new_bin` function.
 * @param subdivisions The number of sub bins/shapes to cut into the bin. [x, y]
 * @see bin_get_infill_size_mm For why wall thickness needs to be subtracted.
 */
module bin_subdivide(bin, subdivisions) {
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    );
    assert(is_valid_2d(subdivisions)
        && subdivisions.x >= 0
        && subdivisions.y >= 0
        );

    num_elements = concat(as_2d(subdivisions), 1);
    infill_size_mm = bin_get_infill_size_mm(bin);

    if(infill_size_mm.z > 0) {
        grid = grid_from_total(num_elements, infill_size_mm, true);
        grid_foreach(grid, true) {
            children();
        }
    }
}

/**
 * @brief Translate to the lower left of a particular grid element.
 * @details [0, 0] is the lower left corner.
 *          Since some functions expect all grid elements to be identical...
 * @param bin A bin created by the `new_bin` function.
 * @param index [x, y] Suppports fractional values.
 * @param exact If [0.5, 0.5] corresponds exactly to the center of a base.
 *        Do not enable this unless you are familiar with how grid_elements interact with perimiters.
 */
module bin_translate(bin, index, exact=false) {
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    );
    assert(is_valid_2d(index));

    index_3d = concat(as_2d(index), 0.5);
    infill_grid = _bin_get_infill_grid(bin, exact);

    grid_translate(infill_grid, index_3d, false)
    children();
}

/**
 * @brief Get the number of bases the bin is composed of.
 * @warning Total size **is not merely multiplying this by `bin_get_bases`.
 * @param bin A bin created by the `new_bin` function.
 * @returns [x, y] Number of bases.
 */
function bin_get_bases(bin) =
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    )
    let(base_grid = bin[1])
    as_2d(grid_get_num_elements(base_grid));

/**
 * @brief Get the dimensions of a single Gridfinity base.
 * @warning Total size **is not merely multiplying this by `bin_get_bases`.
 * @param bin A bin created by the `new_bin` function.
 * @returns [x, y] Size of a single base.
 */
 function bin_get_grid_dimensions(bin) =
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    )
    let(base_grid = bin[1])
    as_2d(grid_get_element_dimensions(base_grid));

/**
 * @brief Get the outer size of the bin.
 * @details Includes base and stacking lip (if enabled).
 * @param bin A bin created by the `new_bin` function.
 * @returns A 3d vector.
 */
function bin_get_bounding_box(bin) =
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    )
    let(base_grid = bin[1])
    let(include_lip = bin[4])
    grid_get_total_dimensions(base_grid)
        + [0, 0, include_lip ? stacking_lip_height() : 0];

/**
 * @brief Get infill dimensions that do not overlap with the walls.
 * @details Wall overlap handling is cricital for cutout chamfers to work correctly.
 * @param bin A bin created by the `new_bin` function.
 * @returns Infill [width, length, height]
 */
function bin_get_infill_size_mm(bin) =
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    )
    let(base_grid = bin[1])
    let(base_thickness=bin[2])
    let(fill_height=bin[3])  //May be negative.
    let(fill_height_total=max(
        fill_height + BASE_HEIGHT - base_thickness, 0))
    let(size_2d=as_2d(grid_get_total_dimensions(base_grid)))
    concat(size_2d-2*[d_wall,d_wall], fill_height_total);

/**
 * @brief Detailed information on bin's overall height.
 * @details Designed for use with `pprint`.
 * @returns A list of [key, value] pairs.
 */
function bin_get_height_breakdown(bin) =
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    )
    let(base_grid = bin[1])
    let(include_lip = bin[4])
    let(bin_height = grid_get_total_dimensions(base_grid).z)
    let(infill_height = bin_get_infill_size_mm(bin).z)
    let(total_height = bin_height + (include_lip ? stacking_lip_height() : 0))
    let(center_height = bin_height - BASE_HEIGHT)
    let(free_space = center_height - infill_height - (include_lip ? STACKING_LIP_SUPPORT_HEIGHT : 0))

    let(heights = [
        ["Total: ", total_height],
        ["* BASE_HEIGHT", BASE_HEIGHT],
        ["  -> BASE_PROFILE_HEIGHT", BASE_PROFILE_HEIGHT],
        ["  -> BASE_BRIDGE_HEIGHT", BASE_BRIDGE_HEIGHT],
        ["* Bin Height", center_height],
        ["  -> Infill", infill_height],
        ["  -> Free space", abs(free_space) > 0.01 ? free_space : 0],
        ["  -> Stacking Lip Support", STACKING_LIP_SUPPORT_HEIGHT],
        ["* Stacking Lip Height (Actual)", stacking_lip_height()],
        ["  -> Stacking Lip Height (Nominal)", STACKING_LIP_HEIGHT],
        ["  -> Stacking Lip Chamfer", stacking_lip_height()-STACKING_LIP_HEIGHT],
    ])

    assert(heights[1][1] == heights[2][1] + heights[3][1],
        "BASE_HEIGHT Calculation Error!")
    assert(heights[8][1] == heights[9][1] + heights[10][1],
        "Stacking Lip Height Calculation Error!")
    assert((heights[5][1] + heights[6][1]
            + (include_lip ? heights[7][1] : 0)
        ) - heights[4][1] < 0.01,
        str("Bin Height Calculation Error!\n",
        "  Expected: ", heights[4][1], "==", heights[5][1],
        "+", heights[6][1], "+", (include_lip ? heights[7][1] : 0),
        "\n"))
    assert(heights[0][1] == heights[1][1] + heights[4][1]
        + (include_lip ? heights[8][1] : 0),
        "Total Height Calculation Error!")
    assert(heights[0][1] == bin_get_bounding_box(bin).z,
        "Total Height Calculation Error!")
    include_lip ? heights: as_list(heights, 7);

/**
 * @brief If the object is a Gridfinity bin.
 * @param bin The object to check.
 */
function is_bin(bin) =
    is_list(bin) && bin[0] == "gridfinity_bin_struct";

/**
 * @brief Internal function. Do not use directly.
 * @param bin A bin created by the `new_bin` function.
 * @param exact If the element center of each element should exactly match the center of the corresponding Gridfinity base.
 * @details Setting exact to true. allows for perfect alignment, but at the cost of the outer elements being slightly smaller.
 */
function _bin_get_infill_grid(bin, exact) =
    assert(is_bin(bin),
        "Not a Gridfinity bin."
    )
    assert(is_bool(exact))
    let(base_grid = bin[1])

    let(infill_size_mm = bin_get_infill_size_mm(bin))
    let(base_perimeter = grid_get_perimeter(base_grid))
    let(infill_element_dimensions=concat(
        bin_get_grid_dimensions(bin),
        infill_size_mm.z))
    let(equal_grid = grid_from_total(
        concat(bin_get_bases(bin), 1), infill_size_mm, true))
    let(exact_grid = grid_from_other(base_grid,
        element_dimensions = infill_element_dimensions,
        perimeter = base_perimeter + _BIN_WALL_PERIMITER))
    // Ensure the calculations are correct.
    assert(grid_get_total_dimensions(equal_grid) == infill_size_mm)
    assert(grid_get_total_dimensions(exact_grid) == infill_size_mm)
    let(infill_grid = exact ? exact_grid : equal_grid)
    infill_grid;

/**
 * @brief Internal variable. Do not use directly.
 */
_BIN_BASE_GAP_MM_PERIMETER = [
    BASE_GAP_MM.x/2,
    BASE_GAP_MM.y/2,
    0,
    BASE_GAP_MM.x/2,
    BASE_GAP_MM.y/2,
    0
];

/**
 * @brief Internal variable. Do not use directly.
 */
 _BIN_WALL_PERIMITER = [
    d_wall,
    d_wall,
    0,
    d_wall,
    d_wall,
    0
 ];
src/core/cutouts.scad
/**
 * @file
 * @brief Modules to cut shapes out of a bin.
 */

include <standard.scad>
use <tab.scad>
use <../helpers/shapes.scad>
use <../helpers/list.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/grid.scad>
use <../helpers/grid_element.scad>

/**
 * @brief "Cutout Grid Scale". Scale [x, y] by the current grid.
 * @details Height is set by the current grid element by default.
 *          Applies `-d_div/2` to X and Y.
 *          **Average element size is not the same as the current element size!**
 * @param size [x, y] in current grid units. z is optional.
 * @param height Override the default height.
 *               If negative, subtract from z-grid height.
 * @return A 3d vector. With z (by default) equal to z-grid (infill) height.
 * @warning Will throw if `grid_element_current()` is not valid.
 */
 function cgs(size=[1, 1], height=0) =
    assert(is_valid_2d(size) && is_positive(size))
    assert(is_num(height))
    let(element=grid_element_current())
    let(grid=grid_element_get_grid(element))
    let(scaled = grid_scale(grid, size))
    assert(is_valid_3d(scaled),
        "grid_element_current() must have at least 3 dimensions.")
    let(divider_mm = [d_div/2, d_div/2, 0])
    let(calculated = as_3d(scaled) - divider_mm)
    assert(calculated.x > 0
        && calculated.y > 0
        && (calculated.z > 0 || height > 0),
        str("Grid Elements too small.  Must be greater than [d_div/2, d_div/2, 0] (", divider_mm, "mm)."))
    [
        calculated.x,
        calculated.y,
        height > 0 ? height : calculated.z + height
    ];

/**
 * @brief A negative of a square compartment with rounded edges,
 *        a chamfered bottom, an optional scoop, and an optional tab.
 * @details Zero position is always the top, with the cutout extending down.
 * @param size_mm [x, y, z] Size of the compartment. In mm.
 * @param scoop_percent 0.0-1.0 How much of a scoop should be present.
 * @param tab_width How wide the tab should be.
 *                  Set to 0 to disable tab.
 * @param tab_angle Determines where the tab is placed.
 *        This will be normalized.
 *        Regardless of compartment dimensions, corners are always at 45 degree intervals.
 *        0: centered +x
 *        46: +y wall, right edge.
 *        90: centered +y
 *        134: +y wall, left edge.
 * @param center_top Default true, cutter [x, y] is centered on the current position.
 *        If false, cutter is in quadrant 1 [+x, +y].
 */
module compartment_cutter(size_mm, scoop_percent=0, tab_width=0, tab_angle=90, center_top=true) {
    assert(is_valid_3d(size_mm) && is_positive(size_mm));
    assert(is_num(scoop_percent));
    assert(is_num(tab_angle)
        || (is_undef(tab_angle) && tab_width == 0));
    assert(is_bool(center_top));

    translate_by = center_top ? [0, 0, 0]
        : [size_mm.x/2, size_mm.y/2, 0];

    color("olive")
    translate(translate_by)
    difference() {
        _half_rounded_square(size_mm);

        if (scoop_percent > 0) {
            _compartment_scoop(
                size_mm+as_3d(TOLLERANCE),
                scoop_percent);
        }

        if (tab_width > 0) {
            _compartment_tab(
                size_mm+as_3d(TOLLERANCE),
                tab_width+TOLLERANCE,
                tab_angle);
        }
    }
}

/**
 * @brief Internal module. Do not use directly.
 * @param size_mm [x, y, z] Size of the compartment. In mm.
 */
module _half_rounded_square(size_mm) {
    assert(is_valid_3d(size_mm)
        && is_positive(size_mm));

    difference(){
        rounded_cube([
            size_mm.x,
            size_mm.y,
            size_mm.z*2,
        ], r_f2, center=true);

        translate([0, 0, size_mm.z/2])
        cube(size_mm+as_3d(TOLLERANCE), center=true);
    }
}

/**
 * @brief Internal module. Do not use directly.
 * @details Add a tab to `cut_compartment`
 * @param size_mm [x, y, z] Size of the compartment. In mm.
 * @param tab_width How wide the tab should be.
 * @param tab_angle Determines where the tab is placed.
 *                  This will be normalized.
 *                  Regardless of compartment dimensions, corners are always at 45 degree intervals.
 */
module _compartment_tab(size_mm, tab_width, tab_angle) {
    assert(is_valid_2d(size_mm) && is_positive(size_mm));
    assert(is_num(tab_angle));
    assert(is_num(tab_width) && tab_width > 0);

    size_2d = as_2d(size_mm);
    normalized_angle = normalize_to_box(size_2d, tab_angle);

    snap_to_edge(size_2d, normalized_angle, tab_width)
    translate([0, 0, -TAB_SIZE.y])
    tab(tab_width);
}

/**
 * @brief Internal module. Do not use directly.
 * @details Add a scoop to `cut_compartment`
 * @param size_mm [x, y, z] Size of the compartment. In mm.
 * @param scoop_percent 0.0-1.0 how of a scoop should be present.
 */
module _compartment_scoop(size_mm, scoop_percent) {
    assert(is_list(size_mm) && len(size_mm) == 3 && size_mm.x >0 && size_mm.y > 0 && size_mm.z > 0);
    assert(is_num(scoop_percent) && scoop_percent > 0.0 && scoop_percent <= 1.0);

    scoop = scoop_percent * size_mm.z/2;

    translate([
        -size_mm.x/2,
        -size_mm.y/2,
        0
    ])
    rotate([0, 90, 0])
    linear_extrude(size_mm.x)
    intersection() {
        // Limit to the compartment's size.
        square([size_mm.z, size_mm.y]);

        difference() {
            translate([size_mm.z, 0])
            mirror([1, 0])
            square(scoop);

            translate([size_mm.z-scoop, scoop])
            circle(scoop);
        }
    }
}

/**
 * @brief Support legacy syntax and options.
 * @details See `compartment_cutter`
 * @param size_mm [x, y, z] Size of the compartment. In mm.
 * @param style_tab [0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
 *        Auto is not supported if `grid_element_current` is not available.
 * @param tab_top_left_only If the tab will only be on the top left compartment.
 *        Only false is supported when `grid_element_current` is not available.
 * @param scoop 0.0-1.0 How much of a scoop should be present.
 */
module cut_compartment_auto(size_mm, style_tab=5, tab_top_left_only=false, scoop_percent=0) {

    // Lambda so `grid_element_current()` is only called when needed.
    // It can throw!
    is_top_left = function()
        let(element = grid_element_current())
        grid_element_is_last_row(element)
        && grid_element_is_first_col(element);

    has_tab = style_tab != 5 && (!tab_top_left_only || is_top_left());
    tab_width = !has_tab ? 0
        : style_tab == 0 ? max(size_mm) : TAB_WIDTH_NOMINAL;
    tab_angle = has_tab ? get_tab_angle(style_tab) : 0;

    compartment_cutter(size_mm, scoop_percent, tab_width, tab_angle);
}

/**
 * @brief A negative of a chamfered cylinder.  Ready to be cut from solid bin.
 * @details Always a 45 degree chamfer.
 *          A cylinder of `radius+chamfer_radius` can be cut from the walls by default as well.
 * @param radius Radius of the cylinder.
 * @param depth How deep the cylinder goes.
 * @param chamfer_radius How far out from the cylinder, the chamfer should extend.
 * @param cut_lip Add an upper cylinder for cutting the stacking lip.
 *                NOTE: This does not bypass safety measures.
 */
module cut_chamfered_cylinder(radius, depth, chamfer_radius = 0, cut_lip=false) {
    assert(is_num(radius) && radius > 0);
    assert(is_num(depth) && depth > 0);
    assert(is_num(chamfer_radius) && chamfer_radius >= 0);
    outer_radius = radius + chamfer_radius;

    color("olive")
    union() {
        translate([0, 0, -depth])
        cylinder(h=depth, r=radius);

        if (cut_lip) {
            //Taller than tallest bin size.
            cylinder(h=10000, r=outer_radius);
        }

       if (chamfer_radius > 0) {
            mirror([0, 0, 1])
            cone(outer_radius, 45, depth);
        }
    }
}

$test_cutouts=true;
if($test_cutouts) {
    $fa = 4;
    $fs = 0.25;

    dim = [45, 20, 25];

    // Internal - Raw Functions
    translate([0, 50, 0]) {
        _half_rounded_square(dim);

        translate([100, 0, 0])
        _compartment_scoop(dim, 1);
        translate([50, 0, 0])
        difference() {
            _half_rounded_square(dim);
            _compartment_scoop(dim+as_3d(TOLLERANCE), 1);
        }
        translate([-100, 0, 0])
        _compartment_tab(dim, TAB_WIDTH_NOMINAL, 90);
        translate([-50, 0, 0])
        difference() {
            _half_rounded_square(dim);
            _compartment_tab(dim+as_3d(TOLLERANCE),
                TAB_WIDTH_NOMINAL, 90);
        }
    }

    // New Syntax
    compartment_cutter(dim);

    translate([50, 0, 0])
    compartment_cutter(dim, 1);
    translate([-50, 0, 0])
    compartment_cutter(dim, 0, TAB_WIDTH_NOMINAL);
    translate([-100, 0, 0])
    compartment_cutter(dim, 1, TAB_WIDTH_NOMINAL);

    // Support legacy syntax
    translate([0, -50, 0]) {
        cut_compartment_auto(dim);
        translate([50, 0, 0])
        cut_compartment_auto(dim, scoop_percent=1);
        translate([-50, 0, 0])
        cut_compartment_auto(dim, style_tab=3);
        translate([-100, 0, 0])
        cut_compartment_auto(dim, style_tab=3, scoop_percent=1);
    }
    // New Syntax - All tab styles (Except Auto)
    translate([0, -100, 0]) {
        compartment_cutter(dim, 0, 0, 0);
        //Right
        translate([50, 0, 0])
        compartment_cutter(dim, 0, TAB_WIDTH_NOMINAL, 46);
        //Left
        translate([-50, 0, 0])
        compartment_cutter(dim, 0, TAB_WIDTH_NOMINAL, 134);
        // Center
        translate([100, 0, 0])
        compartment_cutter(dim, 0, TAB_WIDTH_NOMINAL, 90);
        // Center (full width)
        translate([-100, 0, 0])
        compartment_cutter(dim, 0, max(dim), 90);
    }
    // Legacy Syntax - All tab styles (Except Auto)
    translate([0, -150, 0]) {
        cut_compartment_auto(dim, style_tab=5);
        //Right
        translate([50, 0, 0])
        cut_compartment_auto(dim, style_tab=4);
        //Left
        translate([-50, 0, 0])
        cut_compartment_auto(dim, style_tab=2);
        // Center
        translate([100, 0, 0])
        cut_compartment_auto(dim, style_tab=3);
        // Center (full width)
        translate([-100, 0, 0])
        cut_compartment_auto(dim, style_tab=0);
    }
    // New Syntax - Centered vs top bottom left
    translate([0, 100, 0]) {
        compartment_cutter(dim, 1, TAB_WIDTH_NOMINAL, 90,
            center_top=true);
        translate([0, 50, 0])
        compartment_cutter(dim, 1, TAB_WIDTH_NOMINAL, 90,
            center_top=false);
    }
    //Chamfered hole
    translate([0, 200, 0]) {
        cut_chamfered_cylinder(5, 15);

        translate([20, 0, 0])
        cut_chamfered_cylinder(5, 15, 5);

        translate([40, 0, 0])
        cut_chamfered_cylinder(5, 15, 5, true);
    }
}
src/core/gridfinity-baseplate.scad
/**
 * @File gridfinity-baseplate.scad
 * @Brief Functions and standards to make a SINGLE gridfinity baseplate.
 * @Copyright Arthur Moore 2025 MIT License
 * @WARNING: Where possible, `use` thise file. Constants may change between minor versions.
 */

use <../helpers/generic-helpers.scad>
use <../helpers/shapes.scad>

// ****************************************
// Baseplate constants
// Based on https://gridfinity.xyz/specification/
// ****************************************

/**
 * @Summary Length & Width of a single baseplate.
 */
BASEPLATE_DIMENSIONS = [42, 42];

/**
 * @Summary Minimum height of a baseplate.
 * @Details Ads clearance height to the polygon, and
 *          ensures the base makes contact with the baseplate lip.
 */
BASEPLATE_HEIGHT = 5;

/**
 * @Summary Corner diameter of the outside of the baseplate.
 */
BASEPLATE_OUTER_DIAMETER = 8;

/**
 * @Summary Profile of a Gridfinity baseplate as described in the spec.
 * @Details This is just a line, and will not create a solid polygon.
 *          Does NOT include the clearance height.
 */
_BASEPLATE_PROFILE = [
    [0, 0], // Innermost bottom point
    [0.7, 0.7], // Up and out at a 45 degree angle
    [0.7, (0.7+1.8)], // Straight up
    [(0.7+2.15), (0.7+1.8+2.15)], // Up and out at a 45 degree angle
];

// ****************************************
// Calculations
// ****************************************

/**
 * @Summary Corner radius of the outside of the baseplate.
 */
BASEPLATE_OUTER_RADIUS = BASEPLATE_OUTER_DIAMETER / 2;

///**
// * @Summary Corner radius of the inside of the baseplate.
// * @Details This is also how much _BASEPLATE_PROFILE needs to be translated
// *          to use `sweep_rounded(...)`.
// */
BASEPLATE_INNER_RADIUS = BASEPLATE_OUTER_RADIUS - _BASEPLATE_PROFILE[3].x;

/**
 * @Summary Corner diameter of the inside of the baseplate.
 */
BASEPLATE_INNER_DIAMETER = BASEPLATE_INNER_RADIUS * 2;

// ****************************************
// Implementation Functions
// ****************************************

/**
 * @Summary Polygon of the negative of a baseplate.
 * @Details Includes clearance height, as required by spec.
 *          Ready to use with to use `sweep_rounded(...)`.
 * @param height Height of the baseplate's hollow section.
                 Must be the same as or larger than BASEPLATE_HEIGHT.
 */
module _baseplate_cutter_polygon(height) {
    assert(height >= BASEPLATE_HEIGHT, "_baseplate_cutter_polygon: height may not be less than BASEPLATE_HEIGHT");
    // The minimum height between the baseplate lip and anything below it.
    // Needed to make sure the base always makes contact with the baseplate lip.
    _baseplate_clearance_height = height - _BASEPLATE_PROFILE[3].y;
    assert(_baseplate_clearance_height > 0, "Baseplate too short.");

    translated_line = foreach_add(_BASEPLATE_PROFILE,
        [BASEPLATE_INNER_RADIUS, _baseplate_clearance_height]);

    polygon(concat(translated_line, [
            [0, height],  // Go in to form a solid polygon
            [0, 0],  // Straight down
            [translated_line[0].x, 0] // Out to the translated start.
        ]));
}

// ****************************************
// Exported Functions
// ****************************************

/**
 * @Summary Inner size of the baseplate.
 * @param size [width, length] of a single baseplate.
 *             Only set if deviating from the standard!
 * @Details To be used with `rounded_square(...)` from generic-helpers.
 * @Example `rounded_square(baseplate_inner_size(), BASEPLATE_INNER_RADIUS, center=true);`
 */
function baseplate_inner_size(size=BASEPLATE_DIMENSIONS) = foreach_add(size, BASEPLATE_INNER_DIAMETER - BASEPLATE_OUTER_DIAMETER);

/**
 * @Summary The negative of a single baseplate.
 * @param size [width, length] of a single baseplate.
 *             Only set if deviating from the standard!
 * @param height Height of the baseplate's hollow section.
                 Must be the same as or larger than BASEPLATE_HEIGHT.
 * @Details Use with `difference()`.
 */
module baseplate_cutter(size=BASEPLATE_DIMENSIONS, height=BASEPLATE_HEIGHT) {
    assert(
        is_list(size) &&
        len(size) == 2 &&
        size.x > BASEPLATE_OUTER_DIAMETER &&
        size.y > BASEPLATE_OUTER_DIAMETER,
        "baseplate_cutter: argument 'dimensions' less than BASEPLATE_OUTER_DIAMETER.");
    assert(height >= BASEPLATE_HEIGHT, "baseplate_cutter: height may not be less than BASEPLATE_HEIGHT");

    inner_dimensions = foreach_add(size, -BASEPLATE_OUTER_DIAMETER);

    //Cube's dimensions are set to ensure overlap with `sweep_rounded(...)`
    //without using `rounded_square(...)`.
    inner_size = baseplate_inner_size(size);
    cube_dimensions = [
            inner_size.x - BASEPLATE_INNER_RADIUS,
            inner_size.y - BASEPLATE_INNER_RADIUS,
            height
        ];
    union(){
        sweep_rounded(inner_dimensions){
            _baseplate_cutter_polygon(height);
        }

        translate([0, 0, height/2])
        cube(cube_dimensions, center = true);
    }
}

/**
 * @Summary A single baseplate.
 * @Details Example of how to use `baseplate_cutter()`
 * @param size [width, length] of a single baseplate.
 *             Only set if deviating from the standard!
 * @param height Height of the baseplate's hollow section.
                 Must be the same as or larger than BASEPLATE_HEIGHT.
 */
module single_baseplate(size=BASEPLATE_DIMENSIONS, height=BASEPLATE_HEIGHT) {
    difference() {
        linear_extrude(height)
        rounded_square(size,
            radius = BASEPLATE_OUTER_RADIUS, center=true);

        baseplate_cutter(size, height);
    }
}
src/core/gridfinity-rebuilt-holes.scad
/**
 * @file gridfinity-rebuilt-holes.scad
 * @brief Functions to create different types of holes in an object.
 */

include <standard.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/angles.scad>
use <../helpers/shapes.scad>

/**
 * @brief Wave generation function for wrapping a circle.
 * @param t An angle of the circle.  Between 0 and 360 degrees.
 * @param count The number of **full** waves in a 360 degree circle.
 * @param range **Half** the difference between minimum and maximum values.
 * @param vertical_offset Added to the output.
 *                        When wrapping a circle, radius of that circle.
 * @details
 *    If plotted on an x/y graph this produces a standard sin wave.
 *    Range only seems weird because it describes half a wave.
 *    Mapped by doing [sin(t), cost(t)] * wave_function(...).
 *    When wrapping a circle:
 *      Final Outer radius is (wave_vertical_offset + wave_range).
 *      Final Inner radius is (wave_vertical_offset - wave_range).
 */
function wave_function(t, count, range, vertical_offset) =
    (sin(t * count) * range) + vertical_offset;

/**
 * @brief A circle with crush ribs to give a tighter press fit.
 * @details Extrude and use as a negative modifier.
 *          Idea based on Slant3D's video at 5:20 https://youtu.be/Bd7Yyn61XWQ?t=320
 *          Implementaiton is completely different.
 *          Important: Lower ribs numbers just result in a deformed circle.
 * @param outer_radius Final outer radius.
 * @param inner_radius Final inner radius.
 * @param ribs Number of crush ribs the circle has.
**/
module ribbed_circle(outer_radius, inner_radius, ribs) {
    assert(outer_radius > 0, "outer_radius must be positive");
    assert(inner_radius > 0, "inner_radius must be positive");
    assert(ribs > 0, "ribs must be positive");
    assert(outer_radius > inner_radius, "outer_radius must be larger than inner_radius");

    wave_range = (outer_radius - inner_radius) / 2;
    wave_vertical_offset = inner_radius + wave_range;
    fragments=get_fragments_from_r(wave_vertical_offset);
    degrees_per_fragment = 360/fragments;

    // Circe with a wave wrapped around it
    wrapped_circle = [ for (i = [0:degrees_per_fragment:360])
        [sin(i), cos(i)] * wave_function(i, ribs, wave_range, wave_vertical_offset)
    ];

    polygon(wrapped_circle);
}

/**
 * @brief A cylinder with crush ribs to give a tighter press fit.
 * @details To be used as the negative for a hole.
 * @see ribbed_circle
 * @param outer_radius Outer Radius of the crush ribs.
 * @param inner_radius Inner Radius of the crush ribs.
 * @param height Cylinder's height.
 * @param ribs Number of crush ribs.
 */
module ribbed_cylinder(outer_radius, inner_radius, height, ribs) {
    assert(height > 0, "height must be positive");
    linear_extrude(height)
    ribbed_circle(
        outer_radius,
        inner_radius,
        ribs
    );
}

/**
 * @brief Make a hole printable without suports.
 * @see https://www.youtube.com/watch?v=W8FbHTcB05w
 * @param inner_radius Radius of the inner hole.
 * @param outer_radius Radius of the outer hole.
 * @param outer_height Height of the outer hole.
 * @param layers Number of layers to make printable.
 * @details This is the negative designed to be cut out of the magnet hole.
 *          Use it with `difference()`.
 *          Special handling is done to support a single layer,
 *          and because the last layer (unless there is only one) has a different shape.
 */
module make_hole_printable(inner_radius, outer_radius, outer_height, layers=2) {
    assert(inner_radius > 0, "inner_radius must be positive");
    assert(outer_radius > 0, "outer_radius must be positive");
    assert(layers > 0);

    height_adjustment = outer_height - (layers * LAYER_HEIGHT);

    // Needed, since the last layer should not be used for calculations,
    // unless there is a single layer.
    calculation_layers = max(layers-1, 1);

    cube_height = LAYER_HEIGHT + 2*TOLLERANCE;
    inner_diameter = 2*(inner_radius+TOLLERANCE);
    outer_diameter = 2*(outer_radius+TOLLERANCE);
    per_layer_difference = (outer_diameter-inner_diameter) / calculation_layers;

    initial_matrix = affine_translate([0, 0, cube_height/2-TOLLERANCE + height_adjustment]);

    // Produces data in the form [affine_matrix, [cube_dimensions]]
    // If layers > 1, the last item produced has an invalid "affine_matrix.y", because it is beyond calculation_layers.
    // That is handled in a special case to avoid doing a check every loop.
    cutout_information = [
        for(i=0; i <= layers; i=i+1)
        [
            initial_matrix * affine_translate([0, 0, (i-1)*LAYER_HEIGHT]) *
                affine_rotate([0, 0, is_even(i) ? 90 : 0]),
            [outer_diameter-per_layer_difference*(i-1),
                outer_diameter-per_layer_difference*i,
                cube_height]
        ]
    ];

    difference() {
        translate([0, 0, layers*cube_height/2 + height_adjustment])
        cube([outer_diameter+TOLLERANCE, outer_diameter+TOLLERANCE, layers*cube_height], center = true);

        for (i = [1 : calculation_layers]){
            data = cutout_information[i];
            multmatrix(data[0])
            cube(data[1], center = true);
        }
        if(layers > 1) {
            data = cutout_information[len(cutout_information)-1];
            multmatrix(data[0])
            cube([data[1].x, data[1].x, data[1].z], center = true);
        }
    }
}

/**
* @brief Refined hole based on Printables @grizzie17's Gridfinity Refined
* @details Magnet is pushed in from +X direction, and held in by friction.
*          Small slit on the bottom allows removing the magnet.
* @see https://www.printables.com/model/413761-gridfinity-refined
*/
module refined_hole() {
    refined_offset = LAYER_HEIGHT * REFINED_HOLE_BOTTOM_LAYERS;

    // Poke through - For removing a magnet using a toothpick
    ptl = refined_offset + LAYER_HEIGHT; // Additional layer just in case
    poke_through_height = REFINED_HOLE_HEIGHT + ptl;
    poke_hole_radius = 2.5;
    magic_constant = 5.60;
    poke_hole_center = [-12.53 + magic_constant, 0, -ptl];

    translate([0, 0, refined_offset])
    union() {
        // Magnet hole
        translate([0, -REFINED_HOLE_RADIUS, 0])
        cube([11, REFINED_HOLE_RADIUS*2, REFINED_HOLE_HEIGHT]);
        cylinder(REFINED_HOLE_HEIGHT, r=REFINED_HOLE_RADIUS);

        // Poke hole
        translate([poke_hole_center.x, -poke_hole_radius/2, poke_hole_center.z])
        cube([10 - magic_constant, poke_hole_radius, poke_through_height]);
        translate(poke_hole_center)
        cylinder(poke_through_height, d=poke_hole_radius);
    }
}

/**
 * @brief Create a screw hole
 * @param radius Radius of the hole.
 * @param height Height of the hole.
 * @param supportless If the hole is designed to be printed without supports.
 * @param chamfer_radius If the hole should be chamfered, then how much should be added to radius.  0 means don't chamfer
 * @param chamfer_angle If the hole should be chamfered, then what angle should it be chamfered at.  Ignored if chamfer_radius is 0.
 */
module screw_hole(radius, height, supportless=false, chamfer_radius=0, chamfer_angle = 45) {
    assert(radius > 0);
    assert(height > 0);
    assert(chamfer_radius >= 0);

    union(){
        difference() {
            cylinder(h = height, r = radius);
            if (supportless) {
                rotate([0, 0, 90])
                make_hole_printable(0.5, radius, height, 3);
            }
        }
        if (chamfer_radius > 0) {
            cone(radius + chamfer_radius, chamfer_angle, height);
        }
    }
}

/**
 * @brief Create an options list used to configure bin holes.
 * @param refined_hole Use gridfinity refined hole type.  Not compatible with "magnet_hole".
 * @param magnet_hole Create a hole for a 6mm magnet.
 * @param screw_hole Create a hole for a M3 screw.
 * @param crush_ribs If the magnet hole should have crush ribs for a press fit.
 * @param chamfer Add a chamfer to the magnet/screw hole.
 * @param supportless If the magnet/screw hole should be printed in such a way that the screw hole does not require supports.
 */
function bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false) =
    assert(is_bool(refined_hole))
    assert(is_bool(magnet_hole))
    assert(is_bool(screw_hole))
    assert(is_bool(crush_ribs))
    assert(is_bool(chamfer))
    assert(is_bool(supportless))
    assert(!refined_hole
        || (refined_hole && !magnet_hole),
    "magnet_hole is not compatible with refined_hole")
    [
        "hole_options_struct",
        refined_hole,
        magnet_hole,
        screw_hole,
        crush_ribs,
        chamfer,
        supportless
    ];

/**
 * @brief If the object is a "hole_options".
 * @param hole_options The object to check.
 */
function is_hole_options(hole_options) =
    is_list(hole_options)
    && len(hole_options) == 7
    && hole_options[0] == "hole_options_struct";

/**
 * @brief A single magnet/screw hole.  To be cut out of the base.
 * @details Supports multiple options that can be mixed and matched.
 * @pram hole_options @see bundle_hole_options
 * @param o offset Grows or shrinks the final shapes.  Similar to `scale`, but in mm.
 */
module block_base_hole(hole_options, o=0) {
    assert(is_hole_options(hole_options));
    assert(is_num(o));

    // Destructure the options
    refined_hole = hole_options[1];
    magnet_hole = hole_options[2];
    screw_hole = hole_options[3];
    crush_ribs = hole_options[4];
    chamfer = hole_options[5];
    supportless = hole_options[6];

    screw_radius = SCREW_HOLE_RADIUS - (o/2);
    magnet_radius = MAGNET_HOLE_RADIUS - (o/2);
    magnet_inner_radius = MAGNET_HOLE_CRUSH_RIB_INNER_RADIUS - (o/2);
    screw_depth = BASE_HEIGHT - o;
    // If using supportless / printable mode, need to add additional layers, so they can be removed later.
    supportless_additional_layers = screw_hole ? 2 : 3;
    magnet_depth = MAGNET_HOLE_DEPTH - o +
        (supportless ? supportless_additional_layers*LAYER_HEIGHT : 0);

    union() {
        if(refined_hole) {
            refined_hole();
        }

        if(magnet_hole) {
            difference() {
                if(crush_ribs) {
                    ribbed_cylinder(magnet_radius, magnet_inner_radius, magnet_depth, MAGNET_HOLE_CRUSH_RIB_COUNT);
                } else {
                    cylinder(h = magnet_depth, r=magnet_radius);
                }

                if(supportless) {
                    make_hole_printable(
                    screw_hole ? screw_radius : 1, magnet_radius, magnet_depth, supportless_additional_layers);
                }
            }

            if(chamfer) {
                 cone(magnet_radius + CHAMFER_ADDITIONAL_RADIUS, CHAMFER_ANGLE, MAGNET_HOLE_DEPTH - o);
            }
        }
        if(screw_hole) {
            screw_hole(screw_radius, screw_depth, supportless,
                chamfer ? CHAMFER_ADDITIONAL_RADIUS : 0, CHAMFER_ANGLE);
        }
    }
}

//$fa = 8;
//$fs = 0.25;

if(!is_undef(test_options)){
    block_base_hole(test_options);
}

//block_base_hole(bundle_hole_options(
//    refined_hole=false,
//    magnet_hole=true,
//    screw_hole=true,
//    supportless=true,
//    crush_ribs=false,
//    chamfer=true
//));
//make_hole_printable(1, 3, 0);
src/core/gridfinity-rebuilt-utility.scad
/**
 * @file gridfinity-rebuilt-utility.scad
 * @brief UTILITY FILE, DO NOT EDIT
 *        EDIT OTHER FILES IN REPO FOR RESULTS
 */

include <standard.scad>
use <bin.scad>
use <cutouts.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/grid.scad>
use <../helpers/grid_element.scad>
use <../helpers/list.scad>

// ===== User Modules ===== //

// functions to convert gridz values to mm values

/**
 * @Summary Convert a number from Gridfinity values to mm.
 * @param gridfinityUnit Gridfinity is normally on a base 7 system.
 * @returns The final value in mm. **Excluding** base height, and stacking lip height (if present).
 */
function fromGridfinityUnits(gridfinityUnit) =
    gridfinityUnit * 7;

/**
 * @brief Convert a raw mm height to a specific height format.
 * @param raw_mm Raw input value.
 * @param includes_base Raw input value includes the height of the base.
 * @param includes_lip Raw input value includes the (theoretical) lip height.
 * @returns Height value in mm.  Including base. Excluding lip.
 */
function calculate_bin_height(raw_mm, includes_base=false, includes_lip=false) =
    assert(is_num(raw_mm) && raw_mm >= 0)
    assert(is_bool(includes_base))
    assert(is_bool(includes_lip))
    raw_mm
    + (includes_base ? 0 : BASE_HEIGHT)
    - (includes_lip ? STACKING_LIP_HEIGHT : 0);

_gridz_functions = [
    function(h) fromGridfinityUnits(h),
    function(h) calculate_bin_height(h, false, false),
    function(h) calculate_bin_height(h, true, false),
    function(h) calculate_bin_height(h, true, true),
];

/**
 * @brief Ensure height is a multiple of 7 (Gridfinity Units).
 * @param height_mm The Overall bin's height.
 *                  Including the base!
 * @returns height_mm, if it is a multiple of 7.
 *          Otherwise, the next multiple of 7 from height_mm.
 */
function z_snap(height_mm) =
    height_mm % 7 == 0 ? height_mm
    : height_mm + 7 - height_mm % 7;

/**
 * @brief Calculates the proper height for bins. Three Functions in One.
 * @details This does **not** include the baseplate height.
 * @param z Height value
 * @param gridz_define as explained in gridfinity-rebuilt-bins.scad
 * @param enable_zsnap Automatically snap the bin size to the nearest 7mm increment.
 * @returns Height in mm.  **Excluding** stacking lip height (if present).  **Possibly Excluding** base height.
 */
function height(z, gridz_define, enable_zsnap=true) =
    assert(is_num(z) && z >= 0)
    assert(is_num(gridz_define)
        && gridz_define >= 0
        && gridz_define <= 3)
    assert(is_bool(enable_zsnap))

    let(raw_mm = (_gridz_functions[gridz_define])(z))
    let(total_height = enable_zsnap ? z_snap(raw_mm) : raw_mm)
    max(total_height, BASE_HEIGHT);
src/core/standard.scad


// minimum wall thickness
d_wall = 0.95;

// internal fillet radius
r_f2 = 2.8;

// width of divider between compartments
d_div = 1.2;

/**
 * @brief Size of a single gridfinity unit. [Length, Width] In millimeters.
 */
GRID_DIMENSIONS_MM = [42, 42];

// Tollerance to make sure cuts don't leave a sliver behind,
// and that items are properly connected to each other.
TOLLERANCE = 0.02;

// ****************************************
// Magnet / Screw Hole Constants
// ****************************************
LAYER_HEIGHT = 0.2;
MAGNET_HEIGHT = 2;

SCREW_HOLE_RADIUS = 3 / 2;
MAGNET_HOLE_RADIUS = 6.5 / 2;
MAGNET_HOLE_DEPTH = MAGNET_HEIGHT + (LAYER_HEIGHT * 2);

// distance of hole from side of bin
d_hole_from_side=8;

// Based on https://gridfinity.xyz/specification/
HOLE_DISTANCE_FROM_BOTTOM_EDGE = 4.8;

// Meassured diameter in Fusion360.
// Smaller than the magnet to keep it squeezed.
REFINED_HOLE_RADIUS = 5.86 / 2;
REFINED_HOLE_HEIGHT = MAGNET_HEIGHT - 0.1;
// How many layers are between a Gridfinity Refined Hole and the bottom
REFINED_HOLE_BOTTOM_LAYERS = 2;

// Experimentally chosen for a press fit.
MAGNET_HOLE_CRUSH_RIB_INNER_RADIUS = 5.9 / 2;
// Mostly arbitrarily chosen.
// 30 ribs does not print with a 0.4mm nozzle.
// Anything 5 or under produces a hole that is not round.
MAGNET_HOLE_CRUSH_RIB_COUNT = 8;

// Radius to add when chamfering magnet and screw holes.
CHAMFER_ADDITIONAL_RADIUS = 0.8;
CHAMFER_ANGLE = 45;

// When countersinking the baseplate, how much to add to the screw radius.
BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS = 5/2;
BASEPLATE_SCREW_COUNTERBORE_RADIUS = 5.5/2;
BASEPLATE_SCREW_COUNTERBORE_HEIGHT = 3;

// ****************************************
// Tab Constants
// Arbitrarily chosen.
// ****************************************

/**
 * @brief Maximum width of a tab.
 */
TAB_WIDTH_NOMINAL = 42;

 /**
 * @brief How deep the tab protrudes into the bin.
 * @details External code should use `TAB_SIZE.x` instead.
 */
_tab_depth = 15.85;

/**
 * @brief Angle of the support holding up the tab
 */
 _tab_support_angle = 36;

 /**
 * @brief Additional support height, so the end isn't a sharp angle.
 */
 _tab_support_height = 1.2;

_tab_height = tan(_tab_support_angle) * _tab_depth + _tab_support_height;
TAB_POLYGON = [
    [0, 0], // Start
    [0, _tab_height], // Up
    [_tab_depth, _tab_height], //Out
    [_tab_depth, _tab_height - _tab_support_height] // Prevent a sharp angle
    //Implicit back to start
];

/**
 * @brief Size of the tab.
 * @Details "x": How deep the tab protrudes into the bin.
 *          "y": The height of the tab.
 */
TAB_SIZE = TAB_POLYGON[2];

// ****************************************
// Stacking Lip Constants
// Based on https://gridfinity.xyz/specification/
// Also includes a support base.
// ****************************************

/**
 * @Summary Fillet so the stacking lip does not come to a sharp point.
 */
STACKING_LIP_FILLET_RADIUS = 0.6;

/**
 * @Summary Height of the innermost section. In mm.
 * @Details Used to keep the innermost lip from just being a triangle.
 *          Spec implicitly expects wall width to equal stacking lip depth, so does not define this.
 */
STACKING_LIP_SUPPORT_HEIGHT = 1.2;

/**
 * @Summary Stacking lip as defined in the spec.  No support.
 * @Details This is just a line, and will not create a solid polygon.
 */
STACKING_LIP_LINE = [
    [0, 0], // Inner tip
    [0.7, 0.7], // Go out 45 degrees
    [0.7, (0.7+1.8)], // Vertical increase
    [(0.7+1.9), (0.7+1.8+1.9)], // Go out 45 degrees
];

/**
 * @Summary Size of the stacking lip.
 * @Details "x": How deep the stacking lip protrudes into the bin.
 *               Including wall thickness.
 *          "y": The height of the stacking lip.
 * @WARNING: Height does NOT include STACKING_LIP_SUPPORT_HEIGHT.
 */
STACKING_LIP_SIZE = STACKING_LIP_LINE[3];

/**
 * @brief Height of the stacking lip.
 * @details Height does **not** include STACKING_LIP_SUPPORT_HEIGHT.
 */
STACKING_LIP_HEIGHT = STACKING_LIP_SIZE.y;

_stacking_lip_support_angle = 45;

/**
 * @Summary Calculated value for the overall height of the stacking lip.
 *          Including support.
 */
_stacking_lip_support_height_mm =
    STACKING_LIP_SUPPORT_HEIGHT
    + tan(90 - _stacking_lip_support_angle) * STACKING_LIP_SIZE.x;

/**
 * @Summary Stacking lip with a support. Used to create a polygon.
 * @Details Support is so the stacking lip is not floating in mid air when wall width is less than stacking lip depth.
 */
STACKING_LIP = concat(STACKING_LIP_LINE, [
    [STACKING_LIP_SIZE.x-TOLLERANCE, -_stacking_lip_support_height_mm], // Down to support bottom
    [0, -STACKING_LIP_SUPPORT_HEIGHT], // Up and in (to bottom inner support)
    //[0, 0] // Implicit back to start
]);

// ****************************************
// Base constants
// Based on https://gridfinity.xyz/specification/
// ****************************************

/**
 * @Summary Profile of a Gridfinity base as described in the spec.
 * @Details This is just a line, and will not create a solid polygon.
 */
BASE_PROFILE = [
    [0, 0], // Innermost bottom point
    [0.8, 0.8], // Up and out at a 45 degree angle
    [0.8, (0.8+1.8)], // Straight up
    [(0.8+2.15), (0.8+1.8+2.15)] // Up and out at a 45 degree angle
];

/**
 * @brief Maximum [x, y] values/size of the base.
 */
_base_profile_max_mm= BASE_PROFILE[3];

/**
 * @Summary Corner radius of the top of the base.
 */
BASE_TOP_RADIUS = 7.5 / 2;

/**
 * @Summary Size of the top of the base. [Length, Width]
 * @Details Each unit's base is 41.5mm x 41.5mm
 *          Leaving 0.5mm gap with an l_grid of 42
 */
BASE_TOP_DIMENSIONS = [41.5, 41.5];

/**
 * @Summary How much overhang is expected by the standard per base.
 * @Details There should be a 0.5mm gap between each base.
 *          This must be kept constant, even at half/quarter grid sizes.
 *          Otherwise, they won't fit in normal grids.
 */
BASE_GAP_MM = GRID_DIMENSIONS_MM - BASE_TOP_DIMENSIONS;

/**
 * @brief Height of the base profile.
 * @details Does **not** include the structure tying the bases together.
 */
BASE_PROFILE_HEIGHT = _base_profile_max_mm.y;

/**
 * @Summary Height of the base.
 * @details Includes the structure tying the bases together.
 */
BASE_HEIGHT = 7;

/**
 * @Summary Height of the structure tying the bases together.
 */
BASE_BRIDGE_HEIGHT = BASE_HEIGHT - BASE_PROFILE_HEIGHT;

/**
 * @Summary Corner radius of the bottom of the base.
 * @Details This is also how much BASE_PROFILE needs to be translated
 *          to use `sweep_rounded(...)`.
 */
BASE_BOTTOM_RADIUS = BASE_TOP_RADIUS - _base_profile_max_mm.x;

/**
 * @Summary Dimensions of the bottom of the base. [Length, Width]
 * @Details Supports arbitrary top sizes.
 * @param top_dimensions [Length, Width] of the top of the base.
 */
function base_bottom_dimensions(top_dimensions = BASE_TOP_DIMENSIONS) =
    assert(is_list(top_dimensions) && len(top_dimensions) == 2
        && is_num(top_dimensions.x) && is_num(top_dimensions.y))
    top_dimensions
    - 2*[_base_profile_max_mm.x, _base_profile_max_mm.x];

// ***************
// Gridfinity Refined Thumbscrew
// See https://www.printables.com/model/413761-gridfinity-refined
// ***************

BASE_THUMBSCREW_OUTER_DIAMETER=15;
BASE_THUMBSCREW_PITCH=1.5;


// ****************************************
// Weighted Baseplate
// ****************************************

// Baseplate bottom part height (part added with weigthed=true)
bp_h_bot = 6.4;
// Baseplate bottom cutout rectangle size
bp_cut_size = 21.4;
// Baseplate bottom cutout rectangle depth
bp_cut_depth = 4;
// Baseplate bottom cutout rounded thingy width
bp_rcut_width = 8.5;
// Baseplate bottom cutout rounded thingy left
bp_rcut_length = 4.25;
// Baseplate bottom cutout rounded thingy depth
bp_rcut_depth = 2;

// ****************************************

// radius of cutout for skeletonized baseplate
r_skel = 2;
// minimum baseplate thickness (when skeletonized)
h_skel = 1;


// ****************************************
// Deprecated Values
// Will be removed / re-named in the future.
// ****************************************

// height of tab (yaxis, measured from inner wall)
d_tabh = _tab_depth;

// angle of tab
a_tab = _tab_support_angle;

/**
 * @deprecated Use GRID_DIMENSIONS_MM instead.
 */
l_grid = GRID_DIMENSIONS_MM.x;
src/core/tab.scad
/**
 * @file
 * @brief Functions to create the tab for a gridfinity bin
 */

include <standard.scad>
use <../helpers/angles.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/grid.scad>
use <../helpers/grid_element.scad>

/**
 * @brief Create a tab.
 * @details Exists half in Quadrant 1, and half in Quadrant 4.
 *          Aka +X axis.
 *          Angled so the base is touching the origin.
 * @param width How wide the tab is.
 */
module tab(width = TAB_WIDTH_NOMINAL){
    assert(is_num(width) && width > 0);

    translate([0, width / 2, 0])
    rotate([90, 0, 00])
    linear_extrude(width)
    polygon(TAB_POLYGON);
}

/*
 * @brief Given a centered bounding box and an angle, snap children to an edge of said box based on the angle.
 * @details Rotates children so they are inside the box.
 *          Children must be in Quadrants 1 & 4.  Aka +X axis.
 * @param box_size [x, y] Size of the box.
 * @param angle In degrees.
 * @param child_width Width of the child object(s).
 *                   If provided, the children will be adjusted to stay inside the bounding box.
 */
module snap_to_edge(box_size, angle, child_width = 0) {
    assert(is_list(box_size) && len(box_size) == 2 && box_size.x > 0 && box_size.y > 0);
    assert(is_num(angle));
    assert(is_num(child_width) && child_width >= 0);

    normalized_angle = normalize_angle(angle);
    corner_angle = atan2(box_size.y, box_size.x);

    // If the angle is pointing to one of the sides (+X or -X)
    is_right_side = abs(normalized_angle) <= corner_angle;
    is_left_side = abs(normalized_angle) >= (180 - corner_angle);
    is_top = normalized_angle >= corner_angle && normalized_angle <= (180-corner_angle);
    is_sides = is_right_side || is_left_side;

    // Snap rotation to edges
    rotate_angle = is_right_side ? 180 :
                   is_left_side ? 0:
                   is_top ? -90 : 90;

    translate_sides = [
        box_size.x/2 * signp(normalize_angle(angle + 90)),
        tan(angle) * box_size.x/2 * signp(normalize_angle(angle + 90)),
        0];
    translate_top_bottom = [
        tan(angle-90) * box_size.y/2 * -signp(normalized_angle),
        box_size.y/2 * signp(normalized_angle),
        0];
    translate_by = is_sides ? translate_sides : translate_top_bottom;

    child_max = foreach_add(box_size, -child_width) / 2;

    bounded_translation = [
        is_sides ? translate_by.x : max(min(translate_by.x, child_max.x), -child_max.x),
        !is_sides ? translate_by.y : max(min(translate_by.y, child_max.y), -child_max.y)
    ];

    translate(bounded_translation)
    rotate([0, 0, rotate_angle])
    children();

    if (!is_undef($debug_snap_to_edge) && $debug_snap_to_edge) {
        max_box_size = norm(box_size)/2;
        // Angle marker
        color([is_right_side?1:0, is_left_side?1:0, is_top?1:0, 1])
        rotate([0, 0, angle-90])
        translate([0, max_box_size/2, 0])
        rotate([90, 0, 0])
        cube([1, 1, max_box_size], center=true);

        color("red")
        translate([0,0,40])
        translate(translate_by)
        sphere(2);

        color("blue")
        translate([0,0,30])
        translate(bounded_translation)
        sphere(3);
    }
}

/**
 * @brief Normalize an angle so that 45 degrees points to the corner of a box.
 */
function normalize_to_box(box_size, angle) =
    assert(is_list(box_size) && len(box_size) == 2 && box_size.x > 0 && box_size.y > 0)
    assert(is_num(angle))
    let(na = normalize_angle(angle))
    let(ba = atan2(box_size.x, box_size.y))
    let(is_sides = abs(na) < 45 || abs(na) > 135)
    let(corner_consts = [0, 90 - ba, 90, 90 + ba, 180])
    sign(na) * corner_consts[floor(abs(na)/45)] + (na % 45) * (is_sides ? (90-ba) : ba)/45;

/**
 * @brief Internal variable. Do not use directly.
 * @details Must be a variable, so it can be used as part of a lookup table.
 */
_auto_tab_angle = function()
    let(element = grid_element_current())
    grid_element_is_first_col(element) ? 134 // Left
    : grid_element_is_last_col(element) ? 46 // Right
    : 90; // Center

/**
 * @brief Automatically calculate the tab angle.
 * @details Angle calculations rely on `grid_element_current`.
 *          First column has tabs on the left.
 *          Last column has tabs on the right.
 *          All others have centered tabs.
 */
function auto_tab_angle() = _auto_tab_angle();

/**
 * @brief Convert style_tab to the actual angle.
 * @param style_tab [0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
 * @warning Auto only works when `grid_element_current` is available.
 */
function get_tab_angle(style_tab) =
    assert(is_num(style_tab) && style_tab >=0 && style_tab <= 5)
    let(lookup = [90, _auto_tab_angle, 134, 90 , 46, undef])
    is_function(lookup[style_tab]) ? lookup[style_tab]()
    : lookup[style_tab];

$debug_snap_to_edge = false;
if (!is_undef($debug_snap_to_edge) && $debug_snap_to_edge) {
    b = [200, 100];
    color(c=[0.5, 0, 0, 0.1] ) {
        square(b, center=true);
    }
    raw_angle = $t*360;
    norm_a = normalize_angle(raw_angle);
    na = normalize_to_box(b, raw_angle);
    echo(raw_angle=raw_angle, norm_a=norm_a, na=na, quad=floor(na/90));
    snap_to_edge(b,  na){
        tab(TAB_WIDTH_MAX);
    }
}
src/core/wall.scad
/**
 * @file
 * @brief Functions to create the wall for a Gridfinity bin.
 * @details Has a stacking lip based on https://gridfinity.xyz/specification/
 */

include <standard.scad>
use <../helpers/generic-helpers.scad>
use <../helpers/angles.scad>
use <../helpers/list.scad>
use <../helpers/shapes.scad>

$debug = false;
$test_wall = true;
$fa = 1;
$fs = 0.1; // .01

/*
 * @brief Render a wall of the given size, with a stacking lip.
 * @details Centered on x, y origin.  Bottom is at z == 0.
 *          Adds ~STACKING_LIP_HEIGHT to the height of the bin.
 *          Top is rounded, which reduces the height a bit.
 * @param size [x, y, z] Size of the stacking lip. In mm.
 */
module render_wall(size) {
    assert(
        is_valid_3d(size)
        && size.x > 0
        && size.y > 0
        && size.z >= 0
    );

    grid_size_mm = as_2d(size);

    color("royalblue")
    sweep_rounded(foreach_add(grid_size_mm, -2*BASE_TOP_RADIUS))
    _profile_wall(size.z);

    // Wall below the stacking lip.
    color("orange")
    linear_extrude(size.z)
    difference() {
        rounded_square(grid_size_mm, BASE_TOP_RADIUS, center=true);
        rounded_square(grid_size_mm-2*as_2d(d_wall), BASE_TOP_RADIUS, center=true);
    }
}

/**
 * @brief Get the height of the stacking lip portion.
 * @details This takes the radius into account.
 * @returns The actual height, not nominal.
 */
function stacking_lip_height() =
    let(filleted_line = radius_line_edge(STACKING_LIP, 3, STACKING_LIP_FILLET_RADIUS))
    max([for(l=filleted_line) l.y]);

/**
 * @brief Internal function. Do not use directly.
 * @details If index is first or last element, wraps around.
 *          Also handles common case of first and last points being equal.
 * @param line A list of 2d points.
 * @param index Which point to determine the index of.
 * @returns 2d points before, at, and after.
 */
function _index_to_points(line, index) =
    assert(is_list(line) && len(line) >= 3)
    let(last_index = len(line) - 1)
    assert(is_num(index)
        && index >= 0
        && index <= len(line) - 1)
    let(first_point =
        line[0] != line[last_index] ? line[0]
        : line[1])
    let(last_point =
        line[0] != line[last_index] ? line[last_index]
        : line[last_index-1])
    [
        index==0 ? last_point : line[index-1],  // Before
        line[index], // At
        index==last_index ? first_point
            : line[index+1]  // After
    ];

module _test_index_to_points() {
    square_line = [
        [0, 0],
        [2, 0],
        [2, 2],
        [0, 2],
        [0, 0]
    ];
    assert(_index_to_points(square_line, 0) == [
        [0, 2],
        [0, 0],
        [2, 0],
    ]);
    assert(_index_to_points(square_line, 1) == [
        [0, 0],
        [2, 0],
        [2, 2],
    ]);
    assert(_index_to_points(square_line, 2) == [
        [2, 0],
        [2, 2],
        [0, 2],
    ]);
    assert(_index_to_points(square_line, 3) == [
        [2, 2],
        [0, 2],
        [0, 0]
    ]);
    assert(_index_to_points(square_line, 4) == [
        [0, 2],
        [0, 0],
        [2, 0],
    ]);
}

/**
 * @brief Determine the angle between two lines.
 * @param line A list of 2d points.
 * @param index Which point to determine the index of.
 * @returns -360 to +360.
 */
function point_angle(line, index) =
    let(points = _index_to_points(line, index))
    let(vectors = [
//        points[1] - points[0],  // at - before
        points[0] - points[1],  // before - at
        points[2] - points[1],  // after - at
//        points[1] - points[2],  // at - after
    ])
    atan2(
        cross(vectors[0], vectors[1]),
        vectors[0] * vectors[1]
    );

/**
 * @brief Radius a point along a line.
 * @details Replace 2D edge with a radius.
 *          Method used: tangent, tangent, radius algorithm.
 * @see https://math.stackexchange.com/questions/797828/calculate-center-of-circle-tangent-to-two-lines-in-space
 * @param line A list of points.
 * @param index The point within the line to have a radius applied to it.
 * @param radius The radius to apply.
 * @returns A line with the given point replaced by a semi-circle.
 */
function radius_line_edge(line, index, radius) =
    assert(is_list(line) && len(line) >= 3)
    assert(is_num(index)
        && index >= 0
        && index <= len(line) - 1)
    assert(is_num(radius) && radius > 0)
    let(points = _index_to_points(line, index))
    let(point = points[1])
    let(vectors = [
        points[1] - points[0],  // at - before
        [1, 0] // circle_fragment starts here.
    ])
    // Can be used to determine clockwise or counter clockwise line drawing direction.
    let(angle = point_angle(line, index))
    let(p_angle = positive_angle(angle))
    let(start_angle=atan2(
            cross(vectors[0], vectors[1]),
            vectors[1] * vectors[0]
        ))
    let(psa=positive_angle(start_angle))
    let(sweep_start=sign(angle)*90-psa)

    // Distance from tip to the center point of the circle.
    let(distance_from_edge = radius / sin(abs(angle)/2))
    // Standard line rotation function.
    // [x′,y′]=[x*cosθ-y*sinθ, x*sinθ+y*cosθ]
    let(v=vector_as_unit(vectors[0]))
    let(center_offset = [
        v.x*cos(angle/2) - v.y*sin(angle/2),
        v.x*sin(angle/2) + v.y*cos(angle/2)
    ])
    let(circle_center_point = point - distance_from_edge * center_offset)

//    let(_ = !$debug ? $debug :
//    echo("DEBUG: ---- radius_line_edge ----\n",
//        angle=angle, "\n",
//        p_angle=p_angle, "\n",
//        point=point, "\n",
//        distance_from_edge=distance_from_edge, "\n",
//        center_offset=center_offset, "\n",
//        circle_center_point=circle_center_point, "\n",
//        intersection_distance = center_offset.y, "\n",
//        v0 = vectors[0], "\n",
//        v1 = vectors[1], "\n",
//        start_angle=start_angle, "\n",
//        psa = psa, "\n",
//        sweep_start = sweep_start, "\n",
//    ) $debug)

    let(semicircle = circle_fragment(radius, sweep_start, -sign(angle)*abs(normalize_angle(3*angle))))
//    let(semicircle = [[0,0]])

    let(translated_semicircle = foreach_add(semicircle, circle_center_point))
    //Replace the point with multiple additional points
    concat(
        [for(i=[0:index-1]) line[i]],
        translated_semicircle,
        [for(i=[index+1:len(line)-1]) line[i]]
    );

/**
 * @brief Internal function. Do not use directly.
 * @details Stacking lip with a with a filleted (rounded) top.
 * @details Based on https://gridfinity.xyz/specification/
 *          Also includes a support base.
 */
module _stacking_lip_filleted() {
    filleted_line = radius_line_edge(STACKING_LIP, 3, STACKING_LIP_FILLET_RADIUS);
    polygon(filleted_line);
}

/**
 * @brief A line representing part of a circle.
 * @param radius
 * @param start_angle Where to start the line. In degrees.
 *        0: +X axis.
 *        90: +Y axis
 * @param sweep_by How many degrees the circle should be swept by.
 *        In degrees.  Examples:
 *        -90: Quarter Circle (clockwise)
 *        90: Quarter Circle (counter clockwise)
 *        360: Full Circle
 * @returns A list of points.
 */
function circle_fragment(radius, start_angle=0, sweep_by=360) =
    assert(is_num(radius) && radius > 0)
    assert(is_num(start_angle))
    assert(is_num(sweep_by)
        && sweep_by != 0
        && abs(sweep_by) <= 360)

    let(real_start_angle=start_angle%360)
    let(end_angle = real_start_angle + sweep_by)
    let(fragments = get_fragments_from_r(radius))
    let(degrees_per_fragment = 360/fragments * sign(sweep_by))
//    echo(str("\n",
//        "start_angle=", end_angle, "\n",
//        "real_start_angle=", real_start_angle, "\n",
//        "sweep_by=", sweep_by, "\n",
//        "end_angle=", end_angle, "\n",
//    ))
    [ for (i = [real_start_angle:degrees_per_fragment:end_angle])
        [cos(i), sin(i)] * radius
    ];

/**
 * @brief External wall profile, with a stacking lip.
 * @details Translated so a 90 degree rotation produces the expected outside radius.
 *          Limited to prevent protrusion at low heights.
 * @param height_mm Height of the wall.  Excludes STACKING_LIP_HEIGHT, but **includes** STACKING_LIP_SUPPORT_HEIGHT.
 */
module _profile_wall(height_mm) {
    assert(is_num(height_mm)  && height_mm >=0 );

    filleted_line = radius_line_edge(STACKING_LIP, 3, STACKING_LIP_FILLET_RADIUS);

    line = [
        for(l=filleted_line) [
            l.x + BASE_TOP_RADIUS - STACKING_LIP_SIZE.x,
            max(l.y+height_mm, 0)
        ]
    ];
    polygon(line);
}

module visualize_line(line, print=false) {
    assert(is_list(line) && len(line) > 0);
    assert(is_bool(print));
    seed=0;
    size = len(line)-1;
    new_line = [ for(i=[size:-1:0])
        line[i] + rands(-0.01, 0.01, 2, seed) ];
    final_points = concat(line, new_line);

    if(print) {
        for(i=[0:len(final_points)-1]) {
            echo(str(i, ":", final_points[i]));
        }
    }
    polygon(final_points);
}

if($test_wall) {
    t_square=true;
    t_diamond=true;

//    render_wall([42, 42, 7]);

    translate([2, 2.95147]){
    color("red", alpha=0.1)
    circle(r=0.6);
    circle(r=0.05);
    }

    _test_index_to_points();

    line = circle_fragment(1, 45, -20);
    t_line=[for(l=line) as_2d(concat(l,0,0) * affine_rotate([0, 0, 90]))];
    visualize_line(t_line);

    translate([0, 10, 0])
    _stacking_lip_filleted();

    translate([-10, 0, 0])
    _profile_wall(0);
    translate([-5, 0, 0])
    _profile_wall(10);

    color("blue")
    translate([0, 0, 0])
    visualize_line(STACKING_LIP);
    s_line = radius_line_edge(STACKING_LIP, 3, STACKING_LIP_FILLET_RADIUS);
    visualize_line(s_line);

    color("blue")
    translate([-5, 0, 0])
    visualize_line(reverse(STACKING_LIP));
    s_liner = radius_line_edge(reverse(STACKING_LIP), 2, STACKING_LIP_FILLET_RADIUS);
    translate([-5, 0, 0])
    visualize_line(s_liner);

    if(t_square) {
    square_line = [
        [0, 0],
        [2, 0],
        [2, 2],
        [0, 2],
        [0, 0]
    ];
    sll = len(square_line) - 1;

    translate([5, 0, 0]) {
        visualize_line(square_line);
        translate([0, -3, 0])
        visualize_line(reverse(square_line));
    }

    // Top Right
    translate([9, 0, 0]) {
        visualize_line(radius_line_edge(square_line, 2, 0.5), false);
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(square_line), sll-2, 0.5), false);
    }

    // Bottom Right
    translate([13, 0, 0]) {
        visualize_line(radius_line_edge(square_line, 1, 0.5), false);
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(square_line), sll-1, 0.5), false);
    }

    // Top Left
    translate([17, 0, 0]) {
        visualize_line(radius_line_edge(square_line, 3, 0.5), false);
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(square_line), sll-3, 0.5), false);
    }

    // Bottom Left
    translate([21, 0, 0]) {
        visualize_line(radius_line_edge(square_line, 0, 0.5), false);
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(square_line), sll-4, 0.5), false);
    }
    }

    if(t_diamond) {
    diamond_line = [
        [0, 0],
        [1, -1],
        [2, 0],
        [1, 1],
        [0, 0]
    ];
    dll = len(diamond_line) - 1;

    for(i=[0:4]) {
        color("blue")
        translate([5+i*4, 7.1, 0]) {
            visualize_line(diamond_line);
            translate([0, -3, 0])
            visualize_line(reverse(diamond_line));
        }
    }

    // Bottom Center
    translate([9, 7, 0]) {
        visualize_line(radius_line_edge(diamond_line, 1, 0.5));
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(diamond_line), dll-1, 0.5));
    }

    // Center Right
    translate([13, 7, 0]) {
        visualize_line(radius_line_edge(diamond_line, 2, 0.5));
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(diamond_line), dll-2, 0.5));
    }

    // Top Center
    translate([17, 7, 0]) {
        visualize_line(radius_line_edge(diamond_line, 3, 0.5));
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(diamond_line), dll-3, 0.5));
    }

    // Center Left
    translate([21, 7, 0]) {
        visualize_line(radius_line_edge(diamond_line, 4, 0.5));
        translate([0, -3, 0])
        visualize_line(radius_line_edge(reverse(diamond_line), dll-4, 0.5));
    }
    }
}
src/helpers/angles.scad
/**
 * @file angles.scad
 * @brief Helper functions for angle calculations.
 * @details Not gridfinity specific.
 */

/**
 * @brief `sign` function, but 0 means a positive sign.
 * @description This is useful for applying signs to trig functions.
 * @returns -1 or 1.
 */
function signp(number) =
    assert(is_num(number))
    let(n = sign(number))
    n == 0 ? 1 : n;

/*
 * @brief Convert an angle to between -180 and +180 degrees.
 */
function normalize_angle(angle) =
    assert(is_num(angle))
    let(a = angle%360)
    a > 180 ? a - 360 :
    a < -180 ? a + 360 : a;

/*
 * @brief Convert an angle to between 0 and +360 degrees.
 */
function positive_angle(angle) =
    assert(is_num(angle))
    let(a = angle%360)
    a < 0 ? (a + 360) % 360 : a;

/**
 * @brief Determines the number of fragments in a circle. Aka, Circle resolution.
 * @param r Radius of the circle.
 * @details Recommended function from the manual as a translation of the OpenSCAD function.
 *          Used to improve performance by not rendering every single degree of circles/spheres.
 * @see https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#Circle_resolution:_$fa,_$fs,_and_$fn
 */
function get_fragments_from_r(r) =
    assert(r > 0)
    ($fn>0?($fn>=3?$fn:3):ceil(max(min(360/$fa,r*2*PI/$fs),5)));
src/helpers/generic-helpers.scad
/**
 * @file generic-helpers.scad
 * @brief Generic Helper Functions. Not gridfinity specific.
 */

use <grid.scad>

function clp(x,a,b) = min(max(x,a),b);

function is_even(number) = (number%2)==0;

module copy_mirror(vec=[0,1,0]) {
    children();
    if (vec != [0,0,0])
    mirror(vec)
    children();
}

module pattern_circular(n=2) {
    for (i = [1:n])
    rotate(i*360/n)
    children();
}

/**
 * @brief Unity (no change) affine transformation matrix.
 * @details For use with multmatrix transforms.
 */
unity_matrix = [
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
];

/**
 * @brief Convert a vector into a unit vector.
 * @returns The unit vector.  Where total magnitude is 1.
 */
function vector_as_unit(vector) = vector / norm(vector);

function _affine_rotate_x(angle_x) = [
    [1,  0, 0, 0],
    [0, cos(angle_x), -sin(angle_x), 0],
    [0, sin(angle_x), cos(angle_x), 0],
    [0, 0, 0, 1]
];

function _affine_rotate_y(angle_y) = [
    [cos(angle_y),  0, sin(angle_y), 0],
    [0, 1, 0, 0],
    [-sin(angle_y), 0, cos(angle_y), 0],
    [0, 0, 0, 1]
];

function _affine_rotate_z(angle_z) = [
    [cos(angle_z), -sin(angle_z), 0, 0],
    [sin(angle_z), cos(angle_z), 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
];

/**
 * @brief Affine transformation matrix equivalent of `rotate`
 * @param angle_vector @see `rotate`
 * @details Equivalent to `rotate([0, angle, 0])`
 * @returns An affine transformation matrix for use with `multmatrix()`
 */
function affine_rotate(angle_vector) =
    _affine_rotate_z(angle_vector.z) *
    _affine_rotate_y(angle_vector.y) *
    _affine_rotate_x(angle_vector.x);

/**
 * @brief Affine transformation matrix equivalent of `translate`
 * @param vector @see `translate`
 * @returns An affine transformation matrix for use with `multmatrix()`
 */
function affine_translate(vector) = [
    [1, 0, 0, vector.x],
    [0, 1, 0, vector.y],
    [0, 0, 1, vector.z],
    [0, 0, 0, 1]
];

/**
 * @brief Affine transformation matrix equivalent of `scale`
 * @param vector @see `scale`
 * @returns An affine transformation matrix for use with `multmatrix()`
 */
function affine_scale(vector) = [
    [vector.x, 0, 0, 0],
    [0, vector.y, 0, 0],
    [0, 0, vector.z, 0],
    [0, 0, 0, 1]
];

/**
 * @brief Add something to each element in a list.
 * @param list The list whos elements will be modified.
 * @param to_add
 * @returns A list with `to_add` added to each element in the list.
 */
function foreach_add(list, to_add) =
    assert(is_list(list))
    assert(!is_undef(to_add))
    [for (item = list) item + to_add];

/**
 * @brief Scale each element in a vector by the corresponding element in another vector.
 * @param vector1
 * @param vector2
 * @returns The equivalent of `[vector1.x * vector2.x, vector1.y * vector2.y]`
 */
function vector_scale(vector1, vector2) = assert(len(vector1) == len(vector2))
    [for(i=[0:len(vector1)-1]) vector1[i] * vector2[i] ];


/*
 * @brief If the given vector is a valid 2d vector.
 * @details Only validates the first two elements.
 *          The list could have other things after those.
 */
function is_valid_2d(vector) =
    is_list(vector)
    && len(vector) >= 2
    && is_num(vector[0])
    && is_num(vector[1]);

/*
 * @brief If the given vector is a valid 3d vector.
 * @details This just validates the first three elements.
 *          The list could have other things after those.
 */
function is_valid_3d(vector) =
    is_valid_2d(vector)
    && len(vector) >= 3
    && is_num(vector[2]);

/*
 * @brief If all the elements in a vector are greater than zero.
 */
function is_positive(vector) =
    is_list(vector)
    && min(vector) > 0;

/**
 * @breif Simple helper to print affine matrices in an easier to read manner.
 * @details If a multidimensional matrix is provided, then each item is printed to a separate line.
 * @param object Object to print.
 */
module pprint(object) {
    if(is_list(object) && len(object) != len([for(i=object)each i])) {
        echo("[");
        for(i = object) {
            echo(i);
        };
        echo("]");
    } else {
        echo(object);
    }
}

/**
 * @brief Create a rectangle with rounded corners by sweeping a 2d object along a path.
 * @Details Centered on origin.
 *          Result is on the X,Y plane.
 *          Expects children to be a 2D shape in Quardrant 1 of the X,Y plane.
 * @param size Dimensions of the resulting object.
 *             Either a single number or [width, length]
 */
module sweep_rounded(size) {
    assert((is_num(size) && size > 0) || (
        is_list(size) && len(size) == 2 &&
        is_num(size.x) && size.x > 0 && is_num(size.y) && size.y > 0
        )
    );

    width = is_num(size) ? size : size.x;
    length = is_num(size) ? size : size.y;
    half_width = width/2;
    half_length = length/2;
    path_points = [
        [-half_width, half_length],  //Start
        [half_width, half_length], // Over
        [half_width, -half_length], //Down
        [-half_width, -half_length], // Back over
        [-half_width, half_length]  // Up to start
    ];
    path_vectors = [
        path_points[1] - path_points[0],
        path_points[2] - path_points[1],
        path_points[3] - path_points[2],
        path_points[4] - path_points[3],
    ];
    // These contain the translations, but not the rotations
    // OpenSCAD requires this hacky for loop to get accumulate to work!
    first_translation = affine_translate([path_points[0].y, 0,path_points[0].x]);
    affine_translations = concat([first_translation], [
        for (i = 0, a = first_translation;
            i < len(path_vectors);
            a=a * affine_translate([path_vectors[i].y, 0, path_vectors[i].x]), i=i+1)
        a * affine_translate([path_vectors[i].y, 0, path_vectors[i].x])
    ]);

    // Bring extrusion to the xy plane
    affine_matrix = affine_rotate([90, 0, 90]);

    walls = [
        for (i = [0 : len(path_vectors) - 1])
        affine_matrix * affine_translations[i]
        * affine_rotate([0, atan2(path_vectors[i].y, path_vectors[i].x), 0])
    ];

    union()
    {
        for (i = [0 : len(walls) - 1]){
            multmatrix(walls[i])
            linear_extrude(norm(path_vectors[i]))
            children();

            // Rounded Corners
            multmatrix(walls[i] * affine_rotate([-90, 0, 0]))
            rotate_extrude(angle = 90, convexity = 4)
            children();
        }
    }
}
src/helpers/grid_element.scad
/**
 * @file
 * @brief An element inside a grid ojbect.
 * @details Provides information which can be used as a modifier.
 */

 use <grid.scad>

/**
 * @brief Internal variable.  Do not use directly!
 */
$_grid_element = undef;

/**
 * @brief If the object is a grid_element.
 * @param element The object to check.
 */
function is_grid_element(element) =
    is_list(element)
    && element[0] == "grid_element_struct"
    && len(element) == 4;

/**
 * @brief Information about the given element from `grid_translate` or `grid_foreach`.
 * @details To be used by a child of `grid_translate`.
 * @assert If called anywhere else.
 * @returns An opaque data type to be passed to accessor functions.
 */
function grid_element_current(allow_default = false) =
    assert(is_grid_element($_grid_element),
        "No grid element available.")
    $_grid_element;

/**
 * @brief Parent grid of the current element.
 * @param element An opaque "grid_element" data object.
 * @returns An opaque "grid" data object.
 */
function grid_element_get_grid(element) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid=element[1])
    let(index=element[2])
    let(centered=element[3])
    grid;

/**
 * @brief Multidimensional index of the current element.
 * @param element An opaque "grid_element" data object.
 */
function grid_element_get_index(element) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid=element[1])
    let(index=element[2])
    let(centered=element[3])
    index;

/**
 * @brief If `grid_element_get_position` returns the center of the element.
 * @details Otherwise it returns the bottom left corner.
 * @param element An opaque "grid_element" data object.
 */
function grid_element_is_centered(element) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid=element[1])
    let(index=element[2])
    let(centered=element[3])
    centered;

/**
 * @brief Position of the current element, relative to the parent grid's [0, 0, 0] position.
 * @details Will either be the center of the element, or the lower bottom left corner.
 * @param element An opaque "grid_element" data object.
 * @returns A 3d vector.
 */
function grid_element_get_position(element) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid=element[1])
    let(index=element[2])
    let(centered=element[3])

    let(perimeter = grid_get_perimeter(grid))
    let(raw_element_dimensions = grid_get_element_dimensions(grid))
    let(element_dimensions = grid_element_get_dimensions(element))

    let(raw_element_offset = [
        index.x * raw_element_dimensions.x,
        index.y * raw_element_dimensions.y,
        len(index) >= 3 ? index.z * raw_element_dimensions.z : 0
    ])
    let(perimeter_offset = [
        grid_element_is_first(element, 0) ? perimeter[0] : 0,
        grid_element_is_first(element, 1) ? perimeter[1] : 0,
        len(index) >= 3 && grid_element_is_first(element, 2) ?
            perimeter[2] : 0
    ])
    let(element_offset =[
        element_dimensions.x/2,
        element_dimensions.y/2,
        len(index) >= 3 ? element_dimensions.z/2 : 0
    ])
    grid_get_position_bottom_left(grid)
        + raw_element_offset + perimeter_offset
        + (centered ? element_offset : [0, 0, 0]);

/**
 * @brief Numeric index of the current element.
 * @warning: Ordering in which elements are rendered is **not** finalized and may change.
 * @details Equivalent to:
 *    ```
 *    sum([
 *        index[0],
 *        index[1]*num_elements[0],
 *        index[2]*num_elements[0]*num_elements[1],
 *        ...
 *    ])
 *    ```
 * @param element An opaque "grid_element" data object.
 * @returns A positive integer.
 */
function grid_element_get_sequence_number(element) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid=element[1])
    let(index=element[2])
    let(centered=element[3])
    let(num_elements=grid_get_num_elements(grid))
    let(rank=len(num_elements))

    [for(d=0,a=0,m=1;
            d<rank;
            a=a+index[d]*m,m=m*num_elements[d],d=d+1
        )
        a+index[d] * m
    ][rank-1];

/**
 * @brief Dimensions of the current grid_element.
 * @details Takes the grid perimeter into account.
 * @param element An opaque "grid_element" data object.
 * @returns A vector with the same rank as `grid_get_element_dimensions`.
 */
function grid_element_get_dimensions(element) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid=element[1])
    let(raw_element_dimensions=grid_get_element_dimensions(grid))
    let(perimeter=grid_get_perimeter(grid))
    let(rank=len(raw_element_dimensions))
    let(perimeter_adjustment = [ for(i=[0:rank-1])
        (grid_element_is_first(element, i) ? perimeter[i] : 0)
        + (grid_element_is_last(element, i) ? perimeter[i+rank] : 0)
    ])
    raw_element_dimensions - perimeter_adjustment;

/**
 * @brief If the element's position in the given dimension matches.
 * @param element An opaque "grid_element" data object.
 * @param dimension Dimension to check.
 *                  0: column (x)
 *                  1: row (y)
 *                  2: layer (z)
 *                  So on for higher dimensional grids.
 * @param position Where in the dimension to check.
 *                 0: first
 *                 `num_elements[dimension] - 1`: last.
 */
function grid_element_is_in_position(element, dimension, position) =
    assert(is_grid_element(element), "Not a grid element.")
    assert(is_num(dimension) && dimension >= 0)
    assert(is_num(position) && position >= 0)
    grid_element_get_index(element)[dimension] == position;

/**
 * @brief If this is one of the first elements in a given dimension.
 * @see grid_element_is_in_position
 */
function grid_element_is_first(element, dimension) =
    grid_element_is_in_position(element, dimension, 0);

/**
 * @brief If this is one of the last elements in a given dimension.
 * @see grid_element_is_in_position
 */
function grid_element_is_last(element, dimension) =
    assert(is_grid_element(element), "Not a grid element.")
    let(grid = element[1])
    let(num_elements = grid_get_num_elements(grid))
    assert(dimension < len(num_elements),
        str("Maximum dimension is ", len(num_elements) - 1))
    let(last = num_elements[dimension] - 1)
    grid_element_is_in_position(element, dimension, last);

function grid_element_is_first_col(element) =
    grid_element_is_first(element, 0);

function grid_element_is_first_row(element) =
    grid_element_is_first(element, 1);

function grid_element_is_last_col(element) =
    grid_element_is_last(element, 0);

function grid_element_is_last_row(element) =
    grid_element_is_last(element, 1);

/**
 * @brief Label the element with it's index.
 * @param element An opaque "grid_element" data object.
 * @param label To write on the element.
 *      Automatically passed through `str`.
 *      Accepts functions in the form `fn(element)`.
 */
module grid_element_label(element,
    label=function(e) grid_element_get_index(e)) {
    assert(is_grid_element(element), "Not a grid element.");
    assert(!is_undef(label));

    dimensions = grid_element_get_dimensions(element);
    centered = grid_element_is_centered(element);
    output = str(is_function(label) ? label(element) : label);

    rotate_text = dimensions.x < dimensions.y;
    available_2d = rotate_text ? [dimensions.y, dimensions.x]
        : [dimensions.x, dimensions.y];
    final_available = available_2d - [2.5, 2.5];

    text_size = textmetrics(output,
        halign="center",
        valign="center"
    ).size;
    scaling_factor = [
        final_available.x / text_size.x,
        final_available.y / text_size.y
    ];
    square_scaling_factor =
        [min(scaling_factor), min(scaling_factor), 1];
//    echo(is_rotated=rotate_text,
//        text_size=text_size,
//        scaling_factor=scaling_factor);

    color("black")
    translate(centered ? [0, 0, 0] : dimensions/2)
    rotate([0, 0, rotate_text ? 90 : 0])
    scale(square_scaling_factor)
    text(output,
        halign="center",
        valign="center"
    );
}

/**
 * @brief Color and label a grid element based on it's index.
 * @param element An opaque "grid_element" data object.
 * @param gap Spacing between grid elements.
 * @param alpha 0-1 How transparent the colors are.
 * @param layer_spacing Height difference between elements.
 *                      Helps fix rendering issues.
 */
module debug_grid_element(element, gap=1.0, alpha=1.0, layer_spacing=0.01) {
    assert(is_grid_element(element), "Not a grid element.");
    assert(is_num(gap) && gap >= 0);
    assert(is_num(alpha) && alpha >= 0);
    assert(is_num(layer_spacing));

    rank = len(grid_element_get_dimensions(element));

    if(alpha > 0) {
        if (rank >= 3) {
            _debug_grid_element_3d(element, gap, alpha);
        } else {
            _debug_grid_element_2d(element, gap, alpha, layer_spacing);
        }
    }
}

/**
 * @brief Internal function.  Do not use directly.
 */
module _debug_grid_element_2d(element, gap, alpha, layer_spacing) {
    assert(is_grid_element(element), "Not a grid element.");
    assert(is_num(gap) && gap >= 0);
    assert(is_num(alpha) && alpha > 0);
    assert(is_num(layer_spacing));

    dimensions = grid_element_get_dimensions(element);
    rank = len(dimensions);
    centered = grid_element_is_centered(element);
    assert(rank == 2);

    dimensions_2d = [dimensions.x, dimensions.y];
    gap_2d = [gap, gap] / 2;
    border_2d = [0.1, 0.1] / 2;

    // Zero point dot
    color("grey", alpha)
    translate([0, 0, 2*layer_spacing])
    circle(0.5);

    translate([0, 0, layer_spacing])
    grid_element_label(element);
//    grid_element_label(element, function(e) grid_element_get_sequence_number(e));

    color(_element_color(element), alpha)
    square(dimensions_2d-gap_2d, center=centered);

    // Border
    color("black", alpha)
    translate([0, 0, layer_spacing])
    difference() {
        square(dimensions+border_2d, center=centered);
        translate(concat(border_2d, 0))
        square(dimensions-border_2d, center=centered);
    }
}

/**
 * @brief Internal function.  Do not use directly.
 */
module _debug_grid_element_3d(element, gap, alpha) {
    assert(is_grid_element(element), "Not a grid element.");
    assert(is_num(gap) && gap >= 0);
    assert(is_num(alpha) && alpha >= 0);

    dimensions = grid_element_get_dimensions(element);
    rank = len(dimensions);
    centered = grid_element_is_centered(element);
    assert(rank >= 3);

    border = 0.1;
    dimensions_2d = [dimensions.x, dimensions.y];
    gap_2d = [gap, gap] / 2;
    border_2d = [border, border] / 2;

    // Zero point sphere
    color("grey", alpha)
    sphere(r=0.5);

    color("black")
    linear_extrude(0.01, center=centered)
    grid_element_label(element);
//    grid_element_label(element, function(e) grid_element_get_sequence_number(e));

    // Lightly colored cubes
    color(_element_color(element), min(alpha, 0.1))
    linear_extrude(dimensions.z-gap, center=centered)
    square(dimensions_2d-gap_2d, center=centered);

    // Border
    color("black", alpha)
    linear_extrude(border, center=centered)
    difference() {
        square(dimensions+border_2d, center=centered);
        translate(concat(border_2d, 0))
        square(dimensions-border_2d, center=centered);
    }
}

/**
 * @brief Internal helper function for assigning colors to grid elements.
 */
function _element_color(element) =
    grid_element_is_first_row(element) && grid_element_is_first_col(element) ? "Green"
    : grid_element_is_last_row(element) && grid_element_is_last_col(element) ? "Red"
    : grid_element_is_first_row(element) && grid_element_is_last_col(element) ? "Maroon"
    : grid_element_is_last_row(element) && grid_element_is_first_col(element) ? "Olive"
    : grid_element_is_first_row(element) ? "Blue"
    : grid_element_is_last_row(element) ? "Aqua"
    : grid_element_is_first_col(element) ? "Fuchsia"
    : grid_element_is_last_col(element) ? "Purple"
    : "Yellow";

/*
 * @brief Print information about a grid element.
 */
module print_grid_element(element) {
    assert(is_grid_element(element), "Not a grid element.")

    echo("grid_element:");
    echo(str("  index:\t    ", grid_element_get_index(element)));
    echo(str("  sequence_number:  ", grid_element_get_sequence_number(element)));
    echo(str("  position:\t    ", grid_element_get_position(element)));
    echo(str("  is_centered:\t    ", grid_element_is_centered(element)));
    echo(str("  dimensions:\t    ", grid_element_get_dimensions(element)));
}
src/helpers/grid.scad
/**
 * @file grid.scad
 * @brief 2d grid object.  Used to either create a pattern or to subdivide a larger object.
 */

use <grid_element.scad>

_is_valid_perimeter = function(element_dimensions, perimeter)
    assert(is_list(element_dimensions))
    let(rank=len(element_dimensions))
    is_undef(perimeter) || (
    is_list(perimeter)
    && len(perimeter) == 2 * rank
    // None match the condition.
    && min([for(i=[0:rank-1])
        perimeter[i] + perimeter[i+rank] < element_dimensions[i]
        || (element_dimensions[i] ==0
            && perimeter[i] + perimeter[i+rank] == 0
        ) ? 1 : 0]) != 0
    );

/**
 * @brief Create a pattern of 2d elements.
 * @description Each element is the given size.
 *     The total size is the sum of all elements minus the perimiter.
 *     Perimeter exists to allow a specific spacing between elements, while reducing the overall size.
 *     When perimeter is set, the outer elements dimensions are reduced.
 * @param num_elements [x, y] Number of elements to create.
 * @param @param element_dimensions [length, width] of a single element.
 * @param center Center the entire grid.
 *               Otherwise grid starts at bottom left corner.
 *               Determines the [0, 0] position grid_elements use.
 * @param perimeter [-x, -y, +x, +y, ...]
 *     **Subtracted** from the outer element edges.
 *     Each item represents a side.
 *     Always 2 * len(element_dimensions) items..
 *     Bottom left position is always **outside** the perimeter.
 * @returns An opaque "grid" object.
 */
function new_grid(
    num_elements,
    element_dimensions,
    center=false,
    perimeter=undef
    ) =
    assert(is_list(num_elements)
        && len(num_elements) >= 2
        && min(num_elements) >= 0)
    assert(is_list(element_dimensions)
        && len(element_dimensions) == len(num_elements)
        && min(element_dimensions) >= 0)
    assert(is_bool(center))
    assert(_is_valid_perimeter(element_dimensions, perimeter),
    str("perimeter must have ", len(element_dimensions) * 2," items, and must be smaller than an element."))
    [
        "grid_struct",
        num_elements,
        element_dimensions,
        center,
        !is_undef(perimeter) ? perimeter
            : [for(i=[0:2*len(element_dimensions)-1]) 0]
    ];

/**
 * @brief Subdivide a 2d square into grid of child elements.
 * @param total_dimensions [length, width] of the entire grid.
 * @see new_grid for all other parameters.
 * @returns An opaque "grid" object.
*/
function grid_from_total(
    num_elements,
    total_dimensions,
    center=false,
    perimeter=undef
    ) =
    assert(is_list(num_elements)
        && len(num_elements) >= 2
        && min(num_elements) >= 0)
    assert(is_list(total_dimensions)
        && len(total_dimensions) == len(num_elements)
        && min(total_dimensions) >= 0)
    let(element_dimensions = [
        for(d=[0:len(num_elements)-1])
        num_elements[d] == 0 ? 1
        : total_dimensions[d] / num_elements[d]
    ])
    new_grid(num_elements, element_dimensions, center, perimeter);

/**
 * @brief Create a new grid from an existing one.  Overriding parameters if set.
 * @copydoc new_grid
 */
function grid_from_other(
    grid,
    num_elements=undef,
    element_dimensions=undef,
    center=undef,
    perimeter=undef
    ) =
    assert(is_grid(grid))
    new_grid(
        !is_undef(num_elements) ? num_elements : grid[1],
        !is_undef(element_dimensions) ? element_dimensions : grid[2],
        !is_undef(center) ? center : grid[3],
        !is_undef(perimeter) ? perimeter : grid[4]
    );

function grid_get_num_elements(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(num_elements = grid[1])
    num_elements;

/**
 * @brief Get the total number of elements.
 * @param grid An opaque "grid" data object.
 * @returns A positive integer.
 */
function grid_get_element_count(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(num_elements = grid[1])
    let(rank=len(num_elements))
    [for(i=0,a=1;i<rank;a=a*num_elements[i],i=i+1)
        a*num_elements[i]
    ][rank-1];

/**
 * @brief Theoretical element dimensions.
 * @details Does **NOT** take perimeter into account.
 *          Inner elements are always this size.
 * @param grid An opaque "grid" data object.
 */
function grid_get_element_dimensions(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(element_dimensions = grid[2])
    element_dimensions;

function grid_is_centered(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(centered = grid[3])
    centered;

/**
 * @brief The grid's perimeter.
 * @details This is **subtracted** from the outermost elements.
 * @param grid An opaque "grid" data object.
 * @returns A valid perimeter object.
 */
function grid_get_perimeter(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(perimeter=grid[4])
    perimeter;

/**
 * @brief Average element dimensions.
 * @details **DOES** take perimeter into account.
 *          When a perimiter exists, none of the elements are this exact size.
 * @param grid An opaque "grid" data object.
 * @returns A vector with the same rank as element_dimensions.
 */
function grid_get_average_element_dimensions(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(num_elements = grid[1])
    let(rank = len(num_elements))
    let(total_dimensions = grid_get_total_dimensions(grid))
    [ for(i=[0:rank-1])
        total_dimensions[i] / (
            num_elements[i] != 0 ? num_elements[i] : 1)
    ];

 /**
 * @brief **Not reccomended for most use cases.** Get raw grid size.
 * @details Does **NOT** take perimeter into account.
 * @see grid_get_total_dimensions for the correct function.
 * @param grid An opaque "grid" data object.
 * @returns The raw [length, width, height, ...] of the entire grid.
 */
function grid_get_raw_dimensions(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(num_elements = grid[1])
    let(element_dimensions = grid[2])
    let(rank=len(num_elements))
    [ for(i=[0:rank-1])
        num_elements[i] * element_dimensions[i]
    ];

/**
 * @brief Get the [length, width] of the entire grid.
 * @details Takes perimeter into account.
 * @param grid An opaque "grid" data object.
 * @returns A vector with the same rank as element_dimensions.
 */
function grid_get_total_dimensions(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(perimeter=grid[4])
    let(raw_dimensions=grid_get_raw_dimensions(grid))
    let(rank=len(raw_dimensions))
    raw_dimensions
    - [ for(i=[0:rank-1]) perimeter[i] + perimeter[i+rank] ];

/**
 * @brief The amount to translate by in order to be at the bottom left position of the grid.
 * @details **This is always outside perimeter**, if one exists.
 *    This is the bottom left position of the first element **only** if there is no perimeter.
 * @param grid An opaque "grid" data object.
 * @returns A 3d vector. Ready for use with `translate`.
 */
function grid_get_position_bottom_left(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(centered = grid[3])
    let(half_raw_dimensions=grid_get_raw_dimensions(grid)/2)
    let(rank=len(half_raw_dimensions))
    !centered ? [0, 0, 0]
        : -[
            half_raw_dimensions.x,
            half_raw_dimensions.y,
            rank>=3 ? half_raw_dimensions.z : 0
        ];

/*
 * @brief The amount to translate by in order to be at the center position of the grid.
 * @details Zero point if `is_centered(grid)==true`.
 *     Otherwise assumes zero point is **outside** the perimeter.
 * @param grid An opaque "grid" data object.
 * @returns A 3d vector. Ready for use with `translate`.
 */
function grid_get_position_center(grid) =
    assert(is_grid(grid), "Not a grid.")
    let(centered = grid[3])
    let(half_raw_dimensions=grid_get_raw_dimensions(grid)/2)
    let(rank=len(half_raw_dimensions))
    centered ? [0, 0, 0]
        : [
            half_raw_dimensions.x,
            half_raw_dimensions.y,
            rank>=3 ? half_raw_dimensions.z : 0
        ];

/**
 * @brief If the object is a grid.
 * @param grid The object to check.
 */
function is_grid(grid) =
    is_list(grid) && len(grid) == 5
    && grid[0] == "grid_struct";

/**
 * @brief Get the element at a particular index.
 * @details Uses `floor` to insure all indexes are integers.
 * @param grid An opaque "grid" data object.
 * @param index The multidimensional index of the element.
 * @param center If the element's position should be at the center of the element.  Otherwise, bottom left.
 * @returns An opaque "grid_element" data object.
 */
function grid_get_element(grid, index, center=false) =
    assert(is_grid(grid), "Not a grid.")
    let(num_elements = grid[1])

    assert(is_list(index) && len(index) == len(num_elements),
        str("index must be a list with ", len(num_elements), " items."))
    assert(min(index) >= 0, "index may not contain negative values.")
    assert(min([
            for(d=[0:len(index)-1])
            index[d] < num_elements[d] ? 1 : 0
        ]) == 1,
        str("index must be below ", num_elements))
    assert(is_bool(center))
    let(index_real = [for(i=index) floor(i)])
    [
        "grid_element_struct",
        grid,
        index_real,
        center // Return position as center of the element or not.
    ];

/**
 * @brief Scale the given vector by the **average** grid element dimensions.
 * @details If an dimension is not provided, then it is assumed to be 1.
 *          Calling this without arguments, or with an empty list is equivalent to `grid_get_average_element_dimensions`.
 * @param grid An opaque "grid" data object.
 * @param grid_units vector to scale by.
 * @returns A sacled vector with rank items.
 */
function grid_scale(grid, grid_units=[]) =
    assert(is_grid(grid), "Not a grid.")
    assert(is_list(grid_units))

    let(size_mm=grid_get_average_element_dimensions(grid))
    let(rank = len(size_mm))

    // Ensure grid_units always has at least the same number of dimensions as grid_dimensions_mm.
    let(size = concat(grid_units,
        [for(i=len(grid_units);i<rank;i=i+1) 1]))
    [ for(i=[0:rank-1]) size_mm[i] * size[i] ];

/**
 * @brief Move to a particular element's position within the grid.
 * @details Supports floating point index values.
 *     Children may use `grid_element_current` to obtain the element at the index passed in.
 * @param grid An opaque "grid" data object.
 * @param index The multidimensional index of the element to move to.
 *              If passed a float, final position is translated by the formula `(index-floor(index))*element_dimensions`.
 * @param center Move to the center point of the element.
 *                       Otherwise moves to the bottom left corner.
 */
module grid_translate(grid, index, center=false) {
    element = grid_get_element(grid, index, center);
    $_grid_element = element;

    index_real = grid_element_get_index(element);
    partial_index = index - index_real;
    element_dimensions = grid_get_element_dimensions(grid);
    offset = [for(i=[0:len(index)-1])
        partial_index[i] * element_dimensions[i]
    ];

    translate(grid_element_get_position(element) + offset)
    children();
}

/**
 * @brief Call `grid_translate` on each element in the grid.
 * @details Children may use `grid_element_current` to obtain the current element.
            Creates a grid pattern of elements.
 * @see grid_translate
 * @param grid An opaque "grid" data object.
 */
module grid_foreach(grid, center_elements=false) {
    assert(is_grid(grid), "Not a grid.");
    num_elements = grid[1];

    count = grid_get_element_count(grid);
    rank = len(num_elements);

    for(sequence_number = [0:count-1]) {
        //Goes x -> y -> z
        index = [
            for(d=0,i=sequence_number;
                d<rank;
                i=floor(i/num_elements[d]), d=d+1
            )
            i % num_elements[d]
        ];

//        echo(sequence_number=sequence_number, index=index);
        grid_translate(grid, index, center_elements)
        children();
    }
}

/**
 * @brief Create one child per grid element.
 * @details For use inside `grid_foreach`
 * @WARNING: BETA FEATURE direction and ordering is not guaranteed.
 * @see https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Children
 * > Note that children(), echo() and empty block statements (including ifs) count as $children objects, even if no geometry is present (as of v2017.12.23).
 */
module child_per_element() {
    element = grid_element_current();
    sequence_number = grid_element_get_sequence_number(element);
    children(sequence_number);
}

/**
  * @brief Subdivide a 2d square into grid of child elements.
  * @details Use `grid_element_current` in child elements to obtain details about the current position within the grid.
  *          Use `center_elements=false` for squares and other things in quadrant 1.
  *          Use `center_elements=true` for circles and anything centered.
  * @param num_elements Number of elements to create. [x, y]
  * @param total_dimensions [length, width] of the entire grid.
  * @param center Center the entire grid.
  *               Otherwise grid starts at bottom left corner.
  * @param center_elements Center each element.
  *                        Otherwise each element starts at the bottom left corner.
 */
module subdivide_grid(num_elements, total_dimensions, center=false, center_elements=false) {

    grid = grid_from_total(num_elements, total_dimensions, center);
    grid_foreach(grid, center_elements) {
        children();
    }
}

/**
  * @brief Create a 2d grid pattern of child items.
  * @details Use `grid_element_current` in child elements to obtain details about the current position within the grid.
  *          Use `center_elements=false` for squares and other things in quadrant 1.
  *          Use `center_elements=true` for circles and anything centered.
  * @param num_elements Number of elements to create. [x, y]
  * @param element_dimensions [length, width] of a single element.
  * @param center Center the entire grid.
  *               Otherwise grid starts at bottom left corner.
  * @param center_elements Center each element.
  *                        Otherwise each element starts at the bottom left corner.
 */
module pattern_grid(num_elements, element_dimensions, center=false, center_elements=false) {

    grid = new_grid(num_elements, element_dimensions, center);
    grid_foreach(grid, center_elements) {
        children();
    }
}

/**
 * @brief Print information about the given grid.
 * @param grid An opaque "grid" data object.
 */
module print_grid(grid) {
    assert(is_grid(grid));

    raw_mm = grid_get_raw_dimensions(grid);
    total_mm = grid_get_total_dimensions(grid);
    centered = grid_is_centered(grid);
    position_bottom_left = grid_get_position_bottom_left(grid);
    position_center = grid_get_position_center(grid);

    echo("Grid:");
    echo(str("  element_count:\t     ", grid_get_element_count(grid)));
    echo(str("  num_elements:\t     ", grid_get_num_elements(grid)));
    echo(str("  element_dimensions:  ", grid_get_element_dimensions(grid)));
    echo(str("  average_element_dimensions:  ", grid_get_average_element_dimensions(grid)));
    echo(str("  raw_dimensions:\t     ", raw_mm));
    echo(str("  total_dimensions:\t     ", total_mm));
    echo(str("  is_centered:\t     ", centered));
    echo(str("  position_bottom_left: ", position_bottom_left));
    echo(str("  position_center:\t     ", position_center));
    echo(str("  perimeter:\t     ", grid_get_perimeter(grid)));
}

/**
 * @brief Show the grid's perimeter.
 * @param grid An opaque "grid" data object.
 * @param min_per_side Minimum width per side.
 *                     Ensures the grid can always be visualized.
 *                     0 means do nothing if no perimeter.
 * @returns A 2d object representing the perimeter.
 */
module grid_visualize_perimeter(grid, min_per_side=0.1) {
    assert(is_grid(grid));
    assert(is_num(min_per_side) && min_per_side >= 0);

    raw_mm = grid_get_raw_dimensions(grid);
    rank = len(raw_mm);
    perimeter = grid_get_perimeter(grid);
    position_bottom_left = grid_get_position_bottom_left(grid);

    if(len(raw_mm) < 3) {
        _grid_visualize_perimeter_2d(grid, min_per_side);
    } else {
        translate([0, 0, position_bottom_left.z])
        linear_extrude(max(perimeter[2], min_per_side))
        _grid_visualize_perimeter_2d(grid, min_per_side);

        translate([0, 0, position_bottom_left.z + raw_mm.z])
        mirror([0, 0, 1])
        linear_extrude(max(perimeter[2+rank], min_per_side))
        _grid_visualize_perimeter_2d(grid, min_per_side);
    }
}

/**
 * @brief Internal function.  Do not use directly
 */
module _grid_visualize_perimeter_2d(grid, min_per_side) {
    assert(is_grid(grid));
    assert(is_num(min_per_side) && min_per_side >= 0);

    position_bottom_left = grid_get_position_bottom_left(grid);
    raw_mm = grid_get_raw_dimensions(grid);
    raw_mm_2d = [raw_mm.x, raw_mm.y];
    total_mm = grid_get_total_dimensions(grid);
    perimeter = grid_get_perimeter(grid);

    perimiter_offset = [
        max(perimeter[0], min_per_side),
        max(perimeter[1], min_per_side)
    ];
    inner_size = raw_mm_2d - [
        max(raw_mm.x - total_mm.x, 2 * min_per_side),
        max(raw_mm.y - total_mm.y, 2 * min_per_side)
    ];
    // Ensure logic is correct.
    assert(inner_size.x <= total_mm.x
        && inner_size.y <= total_mm.y);

    // Perimeter
    if(raw_mm != total_mm || min_per_side > 0) {
        translate(position_bottom_left)
        difference() {
            square(raw_mm_2d);

            translate(concat(perimiter_offset, 0))
            square(inner_size);
        }
    }
}

/**
 * @brief Color and label a grid.
 * @param grid An opaque "grid" data object.
 * @param gap Spacing between grid elements.
 * @param alpha 0-1 How transparent the colors are.
 * @param layer_spacing Height difference between elements.
 *                      Helps fix rendering issues.
 * @param print Print data about the current grid/element.
 *     0: Nothing.
 *     1: Grid information.
 *     2: Grid and Element information.
 */
module debug_grid(
    grid,
    gap=1.0,
    alpha=1.0,
    layer_spacing=0.01,
    print=0,
    _center_elements=true
    ) {
    assert(is_num(print)
        && print >= 0
        && print < 3);

    if(print > 0) {
        print_grid(grid);
    }
    grid_foreach(grid, _center_elements) {
        element = grid_element_current();
        debug_grid_element(element, gap=gap, alpha=alpha, layer_spacing=layer_spacing);
        if(print == 2) {
            print_grid_element(element);
        }
    }

    translate([0, 0, 3 * layer_spacing])
    grid_visualize_perimeter(grid);

    //Grid Center
    position_center = grid_get_position_center(grid);
    translate(position_center + [0, 0, 3 * layer_spacing])
    if (len(position_center) == 2) {
        circle(r=0.75);
    } else {
        sphere(r=0.75);
    }
}

$test_grid = true;
//NOTE: The seeming offset between the circles and the squares is because the squares are not centered per element.
if(!is_undef($test_grid) && $test_grid){
    pattern_grid([3, 5], [20, 10], true, false){
        element = grid_element_current();
//        print_grid_element(element);
        debug_grid_element(element, alpha = 0.5);
    }

    translate([0, 0, 2])
    pattern_grid([3, 5], [20, 10], true, true){
        element = grid_element_current();
//        print_grid_element(element);
        color(_element_color(element), alpha=0.3)
        circle(r=4.5);
    }

    translate([0, 0, 3])
    subdivide_grid([3, 5], [60, 50], true, true) {
        circle(1);
    }

    //****************************************//
    // Testing Perimeter

    grid_34 = new_grid([3, 4], [20, 10], false, [1, 2, 3, 4]);
    grid_34_copy = grid_from_other(grid_34);
    grid_34_centered = grid_from_other(grid_34, center=true);
    translate([100, 0, 0])
    debug_grid(grid_34, print=1);
    translate([200, 0, 0])
    debug_grid(grid_34_copy, _center_elements=false);
    translate([300, 0, 0])
    debug_grid(grid_34_centered);

    grid_11 = new_grid([1, 1], [20, 10], false, [4, 3, 2, 1]);
    translate([400, 0, 0])
    debug_grid(grid_11, print=2, _center_elements=false);

    //****************************************//
    grid_341 = new_grid([3, 4, 1], [20, 10, 5], true);
    translate([0, -50, 0])
    debug_grid(grid_341, print=1);

    grid_342 = new_grid([3, 4, 2], [20, 10, 5], true);
    translate([0, -100, 0])
    debug_grid(grid_342, print=1);

    grid_3d2 = grid_from_other(grid_342, center=false);
    translate([0, -175, 0])
    debug_grid(grid_3d2, print=1);

    translate([0, -225, 0])
    debug_grid(grid_3d2, print=0, _center_elements=false);

    grid_3dp = grid_from_other(grid_3d2,
        perimeter=[0, 0, 3, 0, 0, 1]);
    translate([0, -275, 0])
    debug_grid(grid_3dp, print=1);
    //****************************************//

    grid_13 = new_grid([1, 3], [10, 20], true);
    translate([-50, 0, 0]) {
        grid_foreach(grid_13, true)
        child_per_element() {
            circle(r=3);
            square(4, center=true);
            text("text", halign="center");
        }
        //Perimeter should not show
        grid_visualize_perimeter(grid_13, 0);

        //Should create a red circle between text and the square.
        grid_translate(grid_13, [0, 1.5], true){
            element = grid_element_current();
            print_grid_element(element);
            color("red")
            circle(r=1);
        }
    }

    // These should produce nothing.
    translate([0, 0, 10]) {
        pattern_grid([0, 0], [10, 10], true, true) {
            circle(100);
        }
        subdivide_grid([0, 0], [1, 1], true, true) {
            circle(100);
        }
    }

    //****************************************//

    grid_23 = new_grid([2, 3], [10, 20], true);
    assert(grid_scale(grid_23) == [10, 20]);
    assert(grid_scale(grid_23, []) == [10, 20]);
    assert(grid_scale(grid_23, [1]) == [10, 20]);
    assert(grid_scale(grid_23, [1, 1]) == [10, 20]);
    assert(grid_scale(grid_23, [0, 1]) == [0, 20]);
    assert(grid_scale(grid_23, [1, 0]) == [10, 0]);
    assert(grid_scale(grid_23, [0, 0]) == [0, 0]);
    assert(grid_scale(grid_23, [1.5, 1.5]) == [15, 30]);
    assert(grid_scale(grid_23, [2, 2]) == [20, 40]);
}
src/helpers/list.scad
/**
 * @file
 * @brief List helper functions.
 * @details These include some common operations which should probably be included with OpenSCAD.
 */

 /**
 * @brief Reverse the order of elements in a list.
 */
function reverse(list) =
    assert(is_list(list))
    [for(i=[len(list)-1:-1:0]) list[i]];

/**
 * @brief Use an accumulator function to convert a list to a single element.
 * @param list
 * @param accumulator Function to accumulate values.
 *     Must be in the form `accumulated=fn(accumulated, current)`.
 * @param start Initial value.
 */
function aggregate(list, accumulator, start) =
    assert(is_list(list))
    assert(is_function(accumulator))
    assert(!is_undef(start))
    let(count=len(list))
    [
        for(i=0, a=start; i<count; a=accumulator(a, list[i]),i=i+1)
        accumulator(a, list[i])
    ][count-1];

/**
 * @brief Add all list elements together.
 */
function sum(list) =
    let(add = function(a, b) a+b)
    aggregate(list, add, 0);

/**
 * @brief If all list elements return true.
 * @param list
 * @param test_fn Function in the form `bool=fn(value)`
 * @returns boolean
 */
function all(list, test_fn) =
    assert(is_list(list))
    assert(is_function(test_fn))
    min([for(e=list) test_fn(e) ? 1 : 0]) == 1;

/**
 * @brief If any list element returns true.
 * @param list
 * @param test_fn Function in the form `bool=fn(value)`
 * @returns boolean
 */
function any(list, test_fn) =
    assert(is_list(list))
    assert(is_function(test_fn))
    max([for(e=list) test_fn(e) ? 1 : 0]) == 1;

/**
 * @brief If no list element return true.
 * @param list
 * @param test_fn Function in the form `bool=fn(value)`
 * @returns boolean
 */
function none(list, test_fn) =
    !any(list, test_fn);

/**
 * @brief Convert anything to a list.
 * @param object The object to convert.
 *        Lists are trimmed.
 *        All other types are replicated.
 * @param n Final number of objects.
 * @throws If object is a list with less than n elements.
 * @returns A list with n elements.
 */

function as_list(object, n) =
    is_list(object) ?
        assert(len(object) >= n)
        [for(i=[0:n-1]) object[i]]
    : [for(i=[0:n-1]) object];
/**
 * @brief Convert anything to a 2d list.
 */
function as_2d(object) = as_list(object, 2);

/**
 * @brief Convert anything to a 3d list.
 */
function as_3d(object) = as_list(object, 3);

$test_list = true;
if($test_list) {
    assert(reverse([]) == []);
    assert(reverse([1]) == [1]);
    assert(reverse([1, 2, 3, 4]) == [4, 3, 2, 1]);

    assert(sum([1, 2, 3])==6);

    test_f = function(v) v!=0;
    assert(any([1, 1, 1], test_f));
    assert(any([0, 1, 0], test_f));
    assert(!any([0, 0, 0], test_f));
    assert(all([1, 1, 1], test_f));
    assert(!all([0, 1, 0], test_f));
    assert(!all([0, 0, 0], test_f));
    assert(!none([1, 1, 1], test_f));
    assert(!none([0, 1, 0], test_f));
    assert(none([0, 0, 0], test_f));

    assert(as_2d(undef) == [undef, undef]);
    assert(as_2d("abc") == ["abc", "abc"]);
    assert(as_2d(1) == [1, 1]);
    assert(as_2d([1, 2]) == [1, 2]);
    assert(as_2d([1, 2, 3]) == [1, 2]);
}
src/helpers/shapes.scad
/**
 * @file shapes.scad
 * @brief Generic shape modules. Not gridfinity specific.
 */

$debug_shapes=false;

/**
 * @brief Create a cone given a radius and an angle.
 * @param bottom_radius Radius of the bottom of the cone.
 * @param angle Angle as measured from the bottom of the cone.
 * @param max_height Optional maximum height.  Cone will be cut off if higher.
 */
module cone(bottom_radius, angle, max_height=0) {
    assert(bottom_radius > 0);
    assert(angle > 0 && angle <= 90);
    assert(max_height >=0);

    height = tan(angle) * bottom_radius;
    if(max_height == 0 || height < max_height) {
        // Normal Cone
        cylinder(h = height, r1 = bottom_radius, r2 = 0, center = false);
    } else {
        top_angle = 90 - angle;
        top_radius = bottom_radius - tan(top_angle) * max_height;
        cylinder(h = max_height, r1 = bottom_radius, r2 = top_radius, center = false);
    }
}

/**
 * @brief Create `square`, with rounded corners.
 * @param size Same as `square`.
 * @param radius Radius of the corners. 0 is the same as just calling `square`
 * @param center Same as `square`.
 */
module rounded_square(size, radius, center = false) {
    assert(is_num(size) ||
        (is_list(size) && (
            (len(size) == 2 && is_num(size.x) && is_num(size.y))
        ))
    );
    assert(is_num(radius) && radius >= 0);
    assert(is_bool(center));

    // Make sure something is produced.
    if (is_num(size)) {
        assert((size/2) > radius);
    } else {
        assert((size.x/2) > radius && (size.y/2 > radius),
            str("Cannot create a rounded_square smaller than the corner radius (", radius,").")
        );
    }
    size_l = is_num(size) ? [size, size] : size;
    diameter_2d = 2 * [radius, radius];

    offset(radius)
    square(size_l - diameter_2d, center = center);
}

/**
 * @brief Create `cube`, with rounded corners.
 * @param size A positive 3d vector.
 * @param radius Radius of the corners. 0 is the same as just calling `cube`
 * @param center Same as `cube`.
 */
module rounded_cube(size, radius, center=false) {
    assert(is_list(size)
        && len(size) == 3
        && min(size) > 0);
    assert(is_num(radius) && radius >= 0);

    adjusted = size/2 - [radius, radius, radius];
    assert(min(adjusted) >= 0,
        str("All dimensions must be at least 2*radius (",
            2 * radius ,").\n", "  size: ", size, "\n"));

    translate(center ? [0, 0, 0] : size/2)
    if(radius == 0) {
        cube(size, center=true);
    } else {
        // 8 corners
        for(i=[0:1], j=[0:1], k=[0:1])
        mirror([i, 0, 0])
        mirror([0, j, 0])
        mirror([0, 0, k])
        translate(adjusted)
        {
            sphere(r=radius);

            if(adjusted.x > 0)
            color("orange")
            rotate([0, -90, 0])
            cylinder(r=radius, h=adjusted.x);

            if(adjusted.y > 0)
            color("blue")
            rotate([90, 0, 0])
            cylinder(r=radius, h=adjusted.y);

            if(adjusted.z > 0)
            color("green")
            rotate([0, 180, 0])
            cylinder(r=radius, h=adjusted.z);
        }
        //Center Fill
        if(adjusted.y > 0 && adjusted.z > 0)
        cube(size - 2*[0, radius, radius], center=true);
        if(adjusted.x > 0 && adjusted.z > 0)
        cube(size - 2*[radius, 0, radius], center=true);
        if(adjusted.x > 0 && adjusted.y > 0)
        cube(size - 2*[radius, radius, 0], center=true);

//        if($preview && $debug_shapes)
//        color("grey", 0.1)
//        cube(size, center=true);
    }
}

/**
 * @brief Simple donut shape.
 * @details When inner_r=0, called a "horn torus".
 * @param inner_r Inner radius.
 * @param outer_r Outer radius.
 * @param inner_fill Make the inside solid.
 *                   Equivalent to `rounded_cylinder` with height equal to diameter.
 */
module torus(inner_r, outer_r, inner_fill=false) {
    assert(is_num(inner_r) && inner_r >= 0);
    assert(is_num(outer_r) && outer_r > 0);
    assert(outer_r > inner_r);
    radius = (outer_r - inner_r)/2;

    rotate_extrude(360)
    translate([outer_r-radius, 0, 0])
    circle(r=radius);

    if(inner_fill) {
        cylinder(h=2*radius, r=outer_r-radius, center=true);
    }
//    color("blue", 0.5)
//    circle(r=inner_r);
//    color("red", 0.1)
//    circle(r=outer_r);
}

/**
 * @brief
 * @details This is what (wolframcloud)[https://resources.wolframcloud.com/FunctionRepository/resources/RoundedCylinder] calls it.
 * @param height
 * @param radius
 * @param edge_r Radius of the top/bottom edges.
 *               0 is the same as calling `cylinder`.
 * @param center Same as `cylinder(center)`.
 */
module rounded_cylinder(height, radius, edge_r, center=false) {
    assert(is_num(height) && height>0);
    assert(is_num(radius) && radius>0);
    assert(is_num(edge_r) && edge_r>=0);
    assert(is_bool(center));
    assert(height >= 2*edge_r);
    assert(radius >= 2*edge_r);

    inner_radius = radius-2*edge_r;
    inner_height = height-2*edge_r;

    translate([0, 0, edge_r + (center?-height/2:0)]) {
        if(inner_height > 0) {
            cylinder(h=inner_height, r=radius);
        }
        if(edge_r > 0) {
            torus(inner_radius, radius, true);
            translate([0, 0, inner_height])
            torus(inner_radius, radius, true);
        }
    }
//    color("blue", 0.1)
//    cylinder(h=5, r=5);
}

$test_shapes = true;
if(!is_undef($test_shapes) && $test_shapes){
    $fa = 4;
    $fs = 0.1;

    rounded_cube([3, 4, 5], 1, true);
    translate([5, 0, 0])
    rounded_cube([3, 4, 5], 1, false);

    translate([0, 10, 0])
    torus(4, 5);
    translate([0, 10, 0])
    torus(0, 2);
    translate([0, 20, 0])
    torus(4, 5, true);
    translate([0, 30, 0])
    rounded_cylinder(5, 5, 0.25);
    translate([0, 40, 0])
    rounded_cylinder(5, 5, 2.5);
    translate([0, 50, 0])
    rounded_cylinder(5, 5, 0);
    translate([0, 60, 0])
    rounded_cylinder(5, 5, 0.25, center=true);
    translate([0, 70, 0])
    color("blue")
    cylinder(h=5, r=5, center=true);
}

Comments

Context, not a score. Comments never move a model up or down the results.

No comments yet

Printed it? A note about your material and the clearance you landed on saves the next person a failed print.

Sign in to comment