This commit is contained in:
Arthur Moore 2024-04-28 06:05:59 +00:00 committed by GitHub
commit 484bbe36b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 753 additions and 96 deletions

10
.gitignore vendored
View file

@ -1,8 +1,12 @@
ignore/
gridfinity-rebuilt.json
gridfinity-rebuilt-bins.json
stl/
batch/
site/
*.json
/*.json
# From https://github.com/github/gitignore/blob/main/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

View file

@ -5,6 +5,8 @@
function clp(x,a,b) = min(max(x,a),b);
function is_even(number) = (number%2)==0;
module rounded_rectangle(length, width, height, rad) {
linear_extrude(height)
offset(rad)

View file

@ -72,17 +72,30 @@ style_tab = 1; //[0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
style_lip = 0; //[0: Regular lip, 1:remove lip subtractively, 2: remove lip and retain height]
// scoop weight percentage. 0 disables scoop, 1 is regular scoop. Any real number will scale the scoop.
scoop = 1; //[0:0.1:1]
// only cut magnet/screw holes at the corners of the bin to save uneccesary print time
only_corners = false;
/* [Base] */
style_hole = 4; // [0:no holes, 1:magnet holes only, 2: magnet and screw holes - no printable slit, 3: magnet and screw holes - printable slit, 4: Gridfinity Refined hole - no glue needed]
// number of divisions per 1 unit of base along the X axis. (default 1, only use integers. 0 means automatically guess the right division)
div_base_x = 0;
// number of divisions per 1 unit of base along the Y axis. (default 1, only use integers. 0 means automatically guess the right division)
div_base_y = 0;
/* [Base Hole Options] */
// only cut magnet/screw holes at the corners of the bin to save uneccesary print time
only_corners = false;
//Use gridfinity refined hole style. Not compatible with magnet_holes!
refined_hole = true;
// Base will have holes for 6mm Diameter x 2mm high magnets.
magnet_holes = false;
// Base will have holes for M3 screws.
screw_holes = false;
// Magnet holes will have crush ribs to hold the magnet.
crush_ribs = true;
// Magnet holes will have a chamfer to ease insertion.
chamfer_magnet_holes = true;
// Screw holes and magnet holes will be printed so supports are not needed.
printable_hole_top = true;
hole_options = bundle_hole_options(refined_hole, magnet_holes, screw_holes, crush_ribs, chamfer_magnet_holes, printable_hole_top);
// ===== IMPLEMENTATION ===== //
@ -98,7 +111,7 @@ gridfinityInit(gridx, gridy, height(gridz, gridz_define, style_lip, enable_zsnap
cutCylinders(n_divx=cdivx, n_divy=cdivy, cylinder_diameter=cd, cylinder_height=ch, coutout_depth=c_depth, orientation=c_orientation, chamfer=c_chamfer);
}
}
gridfinityBase(gridx, gridy, l_grid, div_base_x, div_base_y, style_hole, only_corners=only_corners);
gridfinityBase(gridx, gridy, l_grid, div_base_x, div_base_y, hole_options, only_corners=only_corners);
}

View file

@ -0,0 +1,279 @@
/**
* @file gridfinity-rebuilt-holes.scad
* @brief Functions to create different types of holes in an object.
*/
include <standard.scad>
use <generic-helpers.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 A simple offset.
* @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;
// Circe with a wave wrapped around it
wrapped_circle = [ for (i = [0: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);
tollerance = 0.01; // Ensure everything is fully removed.
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 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 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 hole.
* @param supportless If the magnet hole should be printed in such a way that the screw hole does not require supports.
*/
function bundle_hole_options(refined_hole=true, magnet_hole=false, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false) =
[refined_hole, magnet_hole, screw_hole, crush_ribs, chamfer, supportless];
/**
* @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
*/
module block_base_hole(hole_options, o=0) {
// Destructure the options
refined_hole = hole_options[0];
magnet_hole = hole_options[1];
screw_hole = hole_options[2];
crush_ribs = hole_options[3];
chamfer = hole_options[4];
supportless = hole_options[5];
// Validate said options
if(refined_hole) {
assert(!magnet_hole, "magnet_hole is not compatible with refined_hole");
}
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 = h_base-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 + MAGNET_HOLE_CHAMFER_ADDITIONAL_RADIUS, MAGNET_HOLE_CHAMFER_ANGLE, magnet_depth);
}
}
if(screw_hole) {
difference() {
cylinder(h = screw_depth, r = screw_radius);
if(supportless) {
rotate([0, 0, 90])
make_hole_printable(0.5, screw_radius, screw_depth, 3);
}
}
}
}
}
//$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=false
//));
//make_hole_printable(1, 3, 0);

View file

@ -6,6 +6,7 @@
include <standard.scad>
use <generic-helpers.scad>
use <gridfinity-rebuilt-holes.scad>
// ===== User Modules ===== //
@ -209,7 +210,7 @@ module profile_base() {
]);
}
module gridfinityBase(gx, gy, l, dx, dy, style_hole, off=0, final_cut=true, only_corners=false) {
module gridfinityBase(gx, gy, l, dx, dy, hole_options, off=0, final_cut=true, only_corners=false) {
dbnxt = [for (i=[1:5]) if (abs(gx*i)%1 < 0.001 || abs(gx*i)%1 > 0.999) i];
dbnyt = [for (i=[1:5]) if (abs(gy*i)%1 < 0.001 || abs(gy*i)%1 > 0.999) i];
dbnx = 1/(dx==0 ? len(dbnxt) > 0 ? dbnxt[0] : 1 : round(dx));
@ -226,61 +227,49 @@ module gridfinityBase(gx, gy, l, dx, dy, style_hole, off=0, final_cut=true, only
translate([0,0,-1])
rounded_rectangle(xx+0.005, yy+0.005, h_base+h_bot/2*10, r_fo1+0.001);
if((style_hole != 0) && (only_corners)) {
if(only_corners) {
difference(){
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
block_base(gx, gy, l, dbnx, dbny, 0, off);
if (style_hole == 4) {
translate([(gx/2)*l_grid - d_hole_from_side, (gy/2) * l_grid - d_hole_from_side, h_slit*2])
refined_hole();
mirror([1, 0, 0])
translate([(gx/2)*l_grid - d_hole_from_side, (gy/2) * l_grid - d_hole_from_side, h_slit*2])
refined_hole();
mirror([0, 1, 0]) {
translate([(gx/2)*l_grid - d_hole_from_side, (gy/2) * l_grid - d_hole_from_side, h_slit*2])
refined_hole();
mirror([1, 0, 0])
translate([(gx/2)*l_grid - d_hole_from_side, (gy/2) * l_grid - d_hole_from_side, h_slit*2])
refined_hole();
copy_mirror([0, 1, 0]) {
copy_mirror([1, 0, 0]) {
translate([
(gx/2)*l_grid - d_hole_from_side,
(gy/2) * l_grid - d_hole_from_side,
0
])
block_base_hole(hole_options, off);
}
}
else {
pattern_linear(2, 2, (gx-1)*l_grid+d_hole, (gy-1)*l_grid+d_hole)
block_base_hole(style_hole, off);
}
}
}
else {
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
block_base(gx, gy, l, dbnx, dbny, style_hole, off);
block_base(gx, gy, l, dbnx, dbny, hole_options, off);
}
}
}
/**
* @brief A single Gridfinity base.
* @brief A single Gridfinity base. With holes (if set).
* @param gx
* @param gy
* @param l
* @param dbnx
* @param dbny
* @param style_hole
* @param hole_options @see block_base_hole.hole_options
* @param off
*/
module block_base(gx, gy, l, dbnx, dbny, style_hole, off) {
module block_base(gx, gy, l, dbnx, dbny, hole_options, off) {
render(convexity = 2)
difference() {
block_base_solid(dbnx, dbny, l, off);
if (style_hole > 0)
pattern_circular(abs(l-d_hole_from_side/2)<0.001?1:4)
if (style_hole == 4)
translate([l/2-d_hole_from_side, l/2-d_hole_from_side, h_slit*2])
refined_hole();
else
translate([l/2-d_hole_from_side, l/2-d_hole_from_side, 0])
block_base_hole(style_hole, off);
}
pattern_circular(abs(l-d_hole_from_side/2)<0.001?1:4)
translate([l/2-d_hole_from_side, l/2-d_hole_from_side, 0])
block_base_hole(hole_options, off);
}
}
/**
@ -311,56 +300,6 @@ module block_base_solid(dbnx, dbny, l, o) {
}
}
module block_base_hole(style_hole, o=0) {
r1 = r_hole1-o/2;
r2 = r_hole2-o/2;
union() {
difference() {
cylinder(h = 2*(h_hole-o+(style_hole==3?h_slit:0)), r=r2, center=true);
if (style_hole==3)
copy_mirror([0,1,0])
translate([-1.5*r2,r1+0.1,h_hole-o])
cube([r2*3,r2*3, 10]);
}
if (style_hole > 1)
cylinder(h = 2*h_base-o, r = r1, center=true);
}
}
module refined_hole() {
/**
* Refined hole based on Printables @grizzie17's Gridfinity Refined
* https://www.printables.com/model/413761-gridfinity-refined
*/
// Meassured magnet hole diameter to be 5.86mm (meassured in fusion360
r = r_hole2-0.32;
// Magnet height
m = 2;
mh = m-0.1;
// Poke through - For removing a magnet using a toothpick
ptl = h_slit*3; // Poke Through Layers
pth = mh+ptl; // Poke Through Height
ptr = 2.5; // Poke Through Radius
union() {
hull() {
// Magnet hole - smaller than the magnet to keep it squeezed
translate([10, -r, 0]) cube([1, r*2, mh]);
cylinder(1.9, r=r);
}
hull() {
// Poke hole
translate([-9+5.60, -ptr/2, -ptl]) cube([1, ptr, pth]);
translate([-12.53+5.60, 0, -ptl]) cylinder(pth, d=ptr);
}
}
}
/**
* @brief Stacking lip based on https://gridfinity.xyz/specification/
* @details Also includes a support base.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -21,18 +21,39 @@ l_grid = 42;
// Per spec, matches radius of upper base section.
r_base = r_fo1;
// screw hole radius
r_hole1 = 1.5;
// magnet hole radius
r_hole2 = 3.25;
// ****************************************
// 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);
// center-to-center distance between holes
d_hole = 26;
// distance of hole from side of bin
d_hole_from_side=8;
// magnet hole depth
h_hole = 2.4;
// slit depth (printer layer height)
h_slit = 0.2;
// 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;
MAGNET_HOLE_CHAMFER_ADDITIONAL_RADIUS = 0.8;
MAGNET_HOLE_CHAMFER_ANGLE = 45;
// ****************************************
// top edge fillet radius
r_f1 = 0.6;

View file

@ -0,0 +1,36 @@
{
"fileFormatVersion": "1",
"parameterSets": {
"Default": {
"$fa": "8",
"$fs": "0.25",
"c_chamfer": "0.5",
"c_depth": "1",
"c_orientation": "2",
"cd": "10",
"cdivx": "0",
"cdivy": "0",
"ch": "1",
"chamfer_magnet_holes": "true",
"crush_ribs": "true",
"div_base_x": "0",
"div_base_y": "0",
"divx": "0",
"divy": "0",
"enable_zsnap": "false",
"gridx": "1",
"gridy": "1",
"gridz": "6",
"gridz_define": "0",
"height_internal": "0",
"magnet_holes": "false",
"only_corners": "false",
"printable_hole_top": "true",
"refined_hole": "true",
"scoop": "0",
"screw_holes": "false",
"style_lip": "0",
"style_tab": "1"
}
}
}

134
tests/openscad_runner.py Normal file
View file

@ -0,0 +1,134 @@
"""
Helpful classes for running OpenScad from Python.
@Copyright Arthur Moore 2024 MIT License
"""
import json
import subprocess
from dataclasses import dataclass, is_dataclass, asdict
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import NamedTuple, Optional
class DataClassJSONEncoder(json.JSONEncoder):
'''Allow json serialization'''
def default(self, o):
if is_dataclass(o):
return asdict(o)
# Let the base class default method raise the TypeError
return super().default(o)
class Vec3(NamedTuple):
'''Simple 3d Vector (x, y, z)'''
x: float
y: float
z: float
@dataclass
class CameraArguments:
"""
Controls the camera position when outputting to png format.
@see `openscad -h`.
"""
translate: Vec3
rotate: Vec3
distance: float
def as_argument(self):
return '--camera=' \
f'{",".join(map(str,self.translate))},{",".join(map(str,self.rotate))},{self.distance}'
@dataclass(kw_only=True, frozen=True)
class ParameterFile:
parameterSets: dict[str, dict]
fileFormatVersion: int = 1
@classmethod
def from_json(cls, *pargs, **nargs):
"""
Wrapper for `json.loads`, with some post-processing.
The Customizer saves everything as strings. --Arthur 2024-04-28
"""
nargs["object_pairs_hook"] = cls.object_pairs_hook
file = ParameterFile(**json.loads(*pargs, **nargs))
assert(file.fileFormatVersion == 1)
return file
@classmethod
def object_pairs_hook(self, pairs: list[tuple]):
'''Fixes customizer turning everything into strings'''
output = dict(pairs)
for (key, value) in output.items():
if(type(value) == str):
if(value == "true"):
output[key] = True
continue
if(value == "false"):
output[key] = False
continue
try:
output[key] = float(value)
except ValueError:
pass
return output
def set_variable_argument(var: str, val: str) -> [str, str]:
"""
Allows setting a variable to a particular value.
@warning value **can** be a function, but this is called for every file, so may generate 'undefined' warnings.
"""
return ['-D', f'{var}={str(val)}']
class OpenScadRunner:
'''Helper to run the openscad binary'''
scad_file_path: Path
openscad_binary_path: Path
image_folder_base: Path
parameters: Optional[dict]
'''If set, a temporary parameter file is created, and used with these variables'''
WINDOWS_DEFAULT_PATH = 'C:\\Program Files\\OpenSCAD\\openscad.exe'
TOP_ANGLE_CAMERA = CameraArguments(Vec3(0,0,0),Vec3(45,0,45),50)
common_arguments = [
#'--hardwarnings', // Does not work when setting variables by using functions
'--enable=fast-csg',
'--enable=predictible-output',
'--imgsize=1280,720',
'--view=axes',
'--projection=ortho',
#"--summary", "all",
#"--summary-file", "-"
] + \
set_variable_argument('$fa', 8) + set_variable_argument('$fs', 0.25)
def __init__(self, file_path: Path):
self.openscad_binary_path = self.WINDOWS_DEFAULT_PATH
self.scad_file_path = file_path
self.image_folder_base = Path('.')
self.parameters = None
def create_image(self, camera_args: CameraArguments, args: [str], image_file_name: str):
"""
Run the code, to create an image.
@Important The only verification is that no errors occured.
There is no verification if the image was created, or the image contents.
"""
assert(self.scad_file_path.exists())
assert(self.image_folder_base.exists())
image_path = self.image_folder_base.joinpath(image_file_name)
command_arguments = self.common_arguments + \
[camera_args.as_argument()] + args + \
["-o", str(image_path), str(self.scad_file_path)]
#print(command_arguments)
if self.parameters != None:
params = ParameterFile(parameterSets={"python_generated": self.parameters})
with NamedTemporaryFile(prefix="gridfinity-rebuilt-", suffix=".json", mode='wt',delete_on_close=False) as file:
json.dump(params, file, sort_keys=True, indent=2, cls=DataClassJSONEncoder)
file.close()
command_arguments += ["-p", file.name, "-P", "python_generated"]
return subprocess.run([self.openscad_binary_path]+command_arguments, check=True)
else:
return subprocess.run([self.openscad_binary_path]+command_arguments, check=True)

142
tests/test_bins.py Normal file
View file

@ -0,0 +1,142 @@
"""
Tests for gridfinity-rebuilt-bins.scad
@Copyright Arthur Moore 2024 MIT License
"""
import dataclasses
import json
import unittest
from pathlib import Path
from tempfile import NamedTemporaryFile
from openscad_runner import *
class TestBinHoles(unittest.TestCase):
"""
Test how a single base looks with holes cut out.
Currently only makes sure code runs, and outputs pictures for manual verification.
"""
@classmethod
def setUpClass(cls):
parameter_file_path = Path("gridfinity-rebuilt-bins.json")
parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text())
cls.default_parameters = parameter_file_data.parameterSets["Default"]
cls.camera_args = CameraArguments(Vec3(0,0,0),Vec3(225,0,225),150)
def setUp(self):
self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-bins.scad'))
self.scad_runner.image_folder_base = Path('../images/base_hole_options/')
self.scad_runner.parameters = self.default_parameters.copy()
def test_no_holes(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = False
vars["screw_hole"] = False
self.scad_runner.create_image(self.camera_args, [], Path('no_holes.png'))
def test_refined_holes(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = True
vars["magnet_hole"] = False
vars["screw_hole"] = False
self.scad_runner.create_image(self.camera_args, [], Path('refined_holes.png'))
def test_refined_and_screw_holes(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = True
vars["magnet_hole"] = False
vars["screw_hole"] = True
vars["printable_hole_top"] = False
self.scad_runner.create_image(self.camera_args, [], Path('refined_and_screw_holes.png'))
def test_screw_holes_plain(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = False
vars["screw_hole"] = True
vars["printable_hole_top"] = False
self.scad_runner.create_image(self.camera_args, [], Path('screw_holes_plain.png'))
def test_screw_holes_printable(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = False
vars["screw_hole"] = True
vars["printable_hole_top"] = True
self.scad_runner.create_image(self.camera_args, [], Path('screw_holes_printable.png'))
def test_magnet_holes_plain(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = False
vars["crush_ribs"] = False
vars["chamfer_magnet_holes"] = False
vars["printable_hole_top"] = False
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_plain.png'))
def test_magnet_holes_chamfered(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = False
vars["crush_ribs"] = False
vars["chamfer_magnet_holes"] = True
vars["printable_hole_top"] = False
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_chamfered.png'))
def test_magnet_holes_printable(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = False
vars["crush_ribs"] = False
vars["chamfer_magnet_holes"] = False
vars["printable_hole_top"] = True
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_printable.png'))
def test_magnet_holes_with_crush_ribs(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = False
vars["crush_ribs"] = True
vars["chamfer_magnet_holes"] = False
vars["printable_hole_top"] = False
self.scad_runner.create_image(self.camera_args, [], Path('magnet_holes_with_crush_ribs.png'))
def test_magnet_and_screw_holes_plain(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = True
vars["crush_ribs"] = False
vars["chamfer_magnet_holes"] = False
vars["printable_hole_top"] = False
self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_plain.png'))
def test_magnet_and_screw_holes_printable(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = True
vars["crush_ribs"] = False
vars["chamfer_magnet_holes"] = False
vars["printable_hole_top"] = True
self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_printable.png'))
def test_magnet_and_screw_holes_all(self):
vars = self.scad_runner.parameters
vars["refined_hole"] = False
vars["magnet_hole"] = True
vars["screw_hole"] = True
vars["crush_ribs"] = True
vars["chamfer_magnet_holes"] = True
vars["printable_hole_top"] = True
self.scad_runner.create_image(self.camera_args, [], Path('magnet_and_screw_holes_all.png'))
if __name__ == '__main__':
unittest.main()

87
tests/test_holes.py Normal file
View file

@ -0,0 +1,87 @@
"""
Tests for gridfinity-rebuilt-holes.scad
@Copyright Arthur Moore 2024 MIT License
"""
from pathlib import Path
from openscad_runner import *
import unittest
class TestHoleCutouts(unittest.TestCase):
"""
Test Hole Cutouts. The negatives used with `difference()` to create a hole.
Currently only makes sure code runs, and outputs pictures for manual verification.
"""
@classmethod
def setUpClass(cls):
cls.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-holes.scad'))
cls.scad_runner.image_folder_base = Path('../images/hole_cutouts/')
def test_refined_hole(self):
"""
refined_hole() is special, since top_angle_camera is not appropriate for it.
"""
camera_args = CameraArguments(Vec3(0,0,0),Vec3(225,0,225),50)
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=true, magnet_hole=false, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(camera_args, test_args, Path('refined_hole.png'))
def test_plain_magnet_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('magnet_hole.png'))
def test_plain_screw_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=true, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('screw_hole.png'))
def test_magnet_and_screw_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('magnet_and_screw_hole.png'))
def test_chamfered_magnet_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=true, supportless=false)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('chamfered_magnet_hole.png'))
def test_magnet_hole_crush_ribs(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=true, chamfer=false, supportless=false)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('magnet_hole_crush_ribs.png'))
def test_magnet_hole_supportless(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=false, supportless=true)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('magnet_hole_supportless.png'))
def test_magnet_and_screw_hole_supportless(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=false, chamfer=false, supportless=true)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('magnet_and_screw_hole_supportless.png'))
def test_all_hole_options(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=true, chamfer=true, supportless=true)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('all_hole_options.png'))
def test_no_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=false, crush_ribs=true, chamfer=true, supportless=true)')
self.scad_runner.create_image(self.scad_runner.TOP_ANGLE_CAMERA,
test_args, Path('no_hole.png'))
if __name__ == '__main__':
unittest.main()