Merge pull request #179 from EmperorArthur/update_holes
Add Multiple Options for Hole Configuration
10
.gitignore
vendored
|
@ -1,8 +1,12 @@
|
||||||
|
|
||||||
ignore/
|
ignore/
|
||||||
gridfinity-rebuilt.json
|
|
||||||
gridfinity-rebuilt-bins.json
|
|
||||||
stl/
|
stl/
|
||||||
batch/
|
batch/
|
||||||
site/
|
site/
|
||||||
*.json
|
/*.json
|
||||||
|
|
||||||
|
# From https://github.com/github/gitignore/blob/main/Python.gitignore
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
function clp(x,a,b) = min(max(x,a),b);
|
function clp(x,a,b) = min(max(x,a),b);
|
||||||
|
|
||||||
|
function is_even(number) = (number%2)==0;
|
||||||
|
|
||||||
module rounded_rectangle(length, width, height, rad) {
|
module rounded_rectangle(length, width, height, rad) {
|
||||||
linear_extrude(height)
|
linear_extrude(height)
|
||||||
offset(rad)
|
offset(rad)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
include <gridfinity-rebuilt-utility.scad>
|
include <gridfinity-rebuilt-utility.scad>
|
||||||
include <standard.scad>
|
include <standard.scad>
|
||||||
|
use <gridfinity-rebuilt-holes.scad>
|
||||||
|
|
||||||
// ===== INFORMATION ===== //
|
// ===== INFORMATION ===== //
|
||||||
/*
|
/*
|
||||||
|
@ -49,23 +50,29 @@ fity = 0; // [-1:0.1:1]
|
||||||
// baseplate styles
|
// baseplate styles
|
||||||
style_plate = 0; // [0: thin, 1:weighted, 2:skeletonized, 3: screw together, 4: screw together minimal]
|
style_plate = 0; // [0: thin, 1:weighted, 2:skeletonized, 3: screw together, 4: screw together minimal]
|
||||||
|
|
||||||
// enable magnet hole
|
|
||||||
enable_magnet = true;
|
|
||||||
|
|
||||||
// hole styles
|
// hole styles
|
||||||
style_hole = 2; // [0:none, 1:countersink, 2:counterbore]
|
style_hole = 2; // [0:none, 1:countersink, 2:counterbore]
|
||||||
|
|
||||||
|
/* [Magnet Hole] */
|
||||||
|
// Baseplate will have holes for 6mm Diameter x 2mm high magnets.
|
||||||
|
enable_magnet = true;
|
||||||
|
// Magnet holes will have crush ribs to hold the magnet.
|
||||||
|
crush_ribs = true;
|
||||||
|
// Magnet holes will have a chamfer to ease insertion.
|
||||||
|
chamfer_holes = true;
|
||||||
|
|
||||||
|
hole_options = bundle_hole_options(refined_hole=false, magnet_hole=enable_magnet, screw_hole=false, crush_ribs=crush_ribs, chamfer=chamfer_holes, supportless=false);
|
||||||
|
|
||||||
// ===== IMPLEMENTATION ===== //
|
// ===== IMPLEMENTATION ===== //
|
||||||
screw_together = (style_plate == 3 || style_plate == 4);
|
|
||||||
|
|
||||||
color("tomato")
|
color("tomato")
|
||||||
gridfinityBaseplate(gridx, gridy, l_grid, distancex, distancey, style_plate, enable_magnet, style_hole, fitx, fity);
|
gridfinityBaseplate(gridx, gridy, l_grid, distancex, distancey, style_plate, hole_options, style_hole, fitx, fity);
|
||||||
|
|
||||||
|
|
||||||
// ===== CONSTRUCTION ===== //
|
// ===== CONSTRUCTION ===== //
|
||||||
|
|
||||||
module gridfinityBaseplate(gridx, gridy, length, dix, diy, sp, sm, sh, fitx, fity) {
|
module gridfinityBaseplate(gridx, gridy, length, dix, diy, sp, hole_options, sh, fitx, fity) {
|
||||||
|
|
||||||
assert(gridx > 0 || dix > 0, "Must have positive x grid amount!");
|
assert(gridx > 0 || dix > 0, "Must have positive x grid amount!");
|
||||||
assert(gridy > 0 || diy > 0, "Must have positive y grid amount!");
|
assert(gridy > 0 || diy > 0, "Must have positive y grid amount!");
|
||||||
|
@ -75,7 +82,7 @@ module gridfinityBaseplate(gridx, gridy, length, dix, diy, sp, sm, sh, fitx, fit
|
||||||
dx = max(gx*length-bp_xy_clearance, dix);
|
dx = max(gx*length-bp_xy_clearance, dix);
|
||||||
dy = max(gy*length-bp_xy_clearance, diy);
|
dy = max(gy*length-bp_xy_clearance, diy);
|
||||||
|
|
||||||
off = calculate_off(sp, sm, sh);
|
off = calculate_offset(sp, hole_options[1], sh);
|
||||||
|
|
||||||
offsetx = dix < dx ? 0 : (gx*length-bp_xy_clearance-dix)/2*fitx*-1;
|
offsetx = dix < dx ? 0 : (gx*length-bp_xy_clearance-dix)/2*fitx*-1;
|
||||||
offsety = diy < dy ? 0 : (gy*length-bp_xy_clearance-diy)/2*fity*-1;
|
offsety = diy < dy ? 0 : (gy*length-bp_xy_clearance-diy)/2*fity*-1;
|
||||||
|
@ -85,7 +92,7 @@ module gridfinityBaseplate(gridx, gridy, length, dix, diy, sp, sm, sh, fitx, fit
|
||||||
mirror([0,0,1])
|
mirror([0,0,1])
|
||||||
rounded_rectangle(dx, dy, h_base+off, r_base);
|
rounded_rectangle(dx, dy, h_base+off, r_base);
|
||||||
|
|
||||||
gridfinityBase(gx, gy, length, 1, 1, 0, 0.5, false);
|
gridfinityBase(gx, gy, length, 1, 1, bundle_hole_options(), 0.5, false);
|
||||||
|
|
||||||
translate([offsetx,offsety,h_base-0.6])
|
translate([offsetx,offsety,h_base-0.6])
|
||||||
rounded_rectangle(dx*2, dy*2, h_base*2, r_base);
|
rounded_rectangle(dx*2, dy*2, h_base*2, r_base);
|
||||||
|
@ -105,33 +112,36 @@ module gridfinityBaseplate(gridx, gridy, length, dix, diy, sp, sm, sh, fitx, fit
|
||||||
|
|
||||||
|
|
||||||
hole_pattern(){
|
hole_pattern(){
|
||||||
if (sm) block_base_hole(1);
|
mirror([0, 0, 1])
|
||||||
|
block_base_hole(hole_options);
|
||||||
|
|
||||||
translate([0,0,-off])
|
translate([0,0,-off-TOLLERANCE])
|
||||||
if (sh == 1) cutter_countersink();
|
if (sh == 1) cutter_countersink();
|
||||||
else if (sh == 2) cutter_counterbore();
|
else if (sh == 2) cutter_counterbore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sp == 3 || sp ==4) cutter_screw_together(gx, gy, off);
|
screw_together = sp == 3 || sp == 4;
|
||||||
|
if (screw_together) cutter_screw_together(gx, gy, off);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculate_off(sp, sm, sh) =
|
function calculate_offset(style_plate, enable_magnet, style_hole) =
|
||||||
screw_together
|
assert(style_plate >=0 && style_plate <=4)
|
||||||
? 6.75
|
let (screw_together = style_plate == 3 || style_plate == 4)
|
||||||
:sp==0
|
screw_together ? 6.75 :
|
||||||
?0
|
style_plate==0 ? 0 :
|
||||||
: sp==1
|
style_plate==1 ? bp_h_bot :
|
||||||
?bp_h_bot
|
calculate_offset_skeletonized(enable_magnet, style_hole);
|
||||||
:h_skel + (sm
|
|
||||||
?h_hole
|
function calculate_offset_skeletonized(enable_magnet, style_hole) =
|
||||||
: 0)+(sh==0
|
h_skel + (enable_magnet ? MAGNET_HOLE_DEPTH : 0) +
|
||||||
? d_screw
|
(
|
||||||
: sh==1
|
style_hole==0 ? d_screw :
|
||||||
?d_cs
|
style_hole==1 ? BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS : // Only works because countersink is at 45 degree angle!
|
||||||
:h_cb);
|
BASEPLATE_SCREW_COUNTERBORE_HEIGHT
|
||||||
|
);
|
||||||
|
|
||||||
module cutter_weight() {
|
module cutter_weight() {
|
||||||
union() {
|
union() {
|
||||||
|
@ -156,23 +166,19 @@ module hole_pattern(){
|
||||||
}
|
}
|
||||||
|
|
||||||
module cutter_countersink(){
|
module cutter_countersink(){
|
||||||
cylinder(r = r_hole1+d_clear, h = 100*h_base, center = true);
|
screw_hole(SCREW_HOLE_RADIUS + d_clear, 2*h_base,
|
||||||
translate([0,0,d_cs])
|
false, BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS);
|
||||||
mirror([0,0,1])
|
|
||||||
hull() {
|
|
||||||
cylinder(h = d_cs+10, r=r_hole1+d_clear);
|
|
||||||
translate([0,0,d_cs])
|
|
||||||
cylinder(h=d_cs+10, r=r_hole1+d_clear+d_cs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module cutter_counterbore(){
|
module cutter_counterbore(){
|
||||||
cylinder(h=100*h_base, r=r_hole1+d_clear, center=true);
|
screw_radius = SCREW_HOLE_RADIUS + d_clear;
|
||||||
difference() {
|
counterbore_height = BASEPLATE_SCREW_COUNTERBORE_HEIGHT + 2*LAYER_HEIGHT;
|
||||||
cylinder(h = 2*(h_cb+0.2), r=r_cb, center=true);
|
union(){
|
||||||
copy_mirror([0,1,0])
|
cylinder(h=2*h_base, r=screw_radius);
|
||||||
translate([-1.5*r_cb,r_hole1+d_clear+0.1,h_cb-h_slit])
|
difference() {
|
||||||
cube([r_cb*3,r_cb*3, 10]);
|
cylinder(h = counterbore_height, r=BASEPLATE_SCREW_COUNTERBORE_RADIUS);
|
||||||
|
make_hole_printable(screw_radius, BASEPLATE_SCREW_COUNTERBORE_RADIUS, counterbore_height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +191,7 @@ module profile_skeleton() {
|
||||||
translate([l_grid/2-d_hole_from_side,l_grid/2-d_hole_from_side,0])
|
translate([l_grid/2-d_hole_from_side,l_grid/2-d_hole_from_side,0])
|
||||||
minkowski() {
|
minkowski() {
|
||||||
square([l,l]);
|
square([l,l]);
|
||||||
circle(r_hole2+r_skel+2);
|
circle(MAGNET_HOLE_RADIUS+r_skel+2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
circle(r_skel);
|
circle(r_skel);
|
||||||
|
|
|
@ -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]
|
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 weight percentage. 0 disables scoop, 1 is regular scoop. Any real number will scale the scoop.
|
||||||
scoop = 1; //[0:0.1:1]
|
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] */
|
/* [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)
|
// 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;
|
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)
|
// 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;
|
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_holes = 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/Screw holes will have a chamfer to ease insertion.
|
||||||
|
chamfer_holes = true;
|
||||||
|
// Magnet/Screw holes will be printed so supports are not needed.
|
||||||
|
printable_hole_top = true;
|
||||||
|
|
||||||
|
hole_options = bundle_hole_options(refined_holes, magnet_holes, screw_holes, crush_ribs, chamfer_holes, printable_hole_top);
|
||||||
|
|
||||||
// ===== IMPLEMENTATION ===== //
|
// ===== 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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
316
gridfinity-rebuilt-holes.scad
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
/**
|
||||||
|
* @file gridfinity-rebuilt-holes.scad
|
||||||
|
* @brief Functions to create different types of holes in an object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
include <standard.scad>
|
||||||
|
use <generic-helpers.scad>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
|
||||||
|
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 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) =
|
||||||
|
[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) {
|
||||||
|
assert(is_list(hole_options));
|
||||||
|
|
||||||
|
// 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 + 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);
|
|
@ -41,9 +41,6 @@ gridz_define = 0; // [0:gridz is the height of bins in units of 7mm increments -
|
||||||
style_tab = 1; //[0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
|
style_tab = 1; //[0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
|
||||||
|
|
||||||
/* [Base] */
|
/* [Base] */
|
||||||
style_hole = 0; // [0:no holes, 1:magnet holes only, 2: magnet and screw holes - no printable slit, 3: magnet and screw holes - printable slit]
|
|
||||||
// only cut magnet/screw holes at the corners of the bin to save uneccesary print time
|
|
||||||
only_corners = false;
|
|
||||||
// number of divisions per 1 unit of base along the X axis. (default 1, only use integers. 0 means automatically guess the right division)
|
// 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;
|
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)
|
// number of divisions per 1 unit of base along the Y axis. (default 1, only use integers. 0 means automatically guess the right division)
|
||||||
|
@ -51,22 +48,40 @@ div_base_y = 0;
|
||||||
// thickness of bottom layer
|
// thickness of bottom layer
|
||||||
bottom_layer = 1;
|
bottom_layer = 1;
|
||||||
|
|
||||||
|
/* [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_holes = false;
|
||||||
|
// Base will have holes for 6mm Diameter x 2mm high magnets.
|
||||||
|
magnet_holes = true;
|
||||||
|
// Base will have holes for M3 screws.
|
||||||
|
screw_holes = true;
|
||||||
|
// Magnet holes will have crush ribs to hold the magnet.
|
||||||
|
crush_ribs = true;
|
||||||
|
// Magnet/Screw holes will have a chamfer to ease insertion.
|
||||||
|
chamfer_holes = true;
|
||||||
|
// Magnet/Screw holes will be printed so supports are not needed.
|
||||||
|
printable_hole_top = true;
|
||||||
|
|
||||||
|
hole_options = bundle_hole_options(refined_holes, magnet_holes, screw_holes, crush_ribs, chamfer_holes, printable_hole_top);
|
||||||
|
|
||||||
// ===== IMPLEMENTATION ===== //
|
// ===== IMPLEMENTATION ===== //
|
||||||
|
|
||||||
// Input all the cutter types in here
|
// Input all the cutter types in here
|
||||||
color("tomato")
|
color("tomato")
|
||||||
gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap, l_grid, div_base_x, div_base_y, style_hole, only_corners) {
|
gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap, l_grid, div_base_x, div_base_y, hole_options, only_corners) {
|
||||||
cutEqual(n_divx = divx, n_divy = divy, style_tab = style_tab, scoop_weight = 0);
|
cutEqual(n_divx = divx, n_divy = divy, style_tab = style_tab, scoop_weight = 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CONSTRUCTION ===== //
|
// ===== CONSTRUCTION ===== //
|
||||||
|
|
||||||
module gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap, length, div_base_x, div_base_y, style_hole, only_corners) {
|
module gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap, length, div_base_x, div_base_y, style_hole, only_corners) {
|
||||||
|
height_mm = height(gridz, gridz_define, style_lip, enable_zsnap);
|
||||||
union() {
|
union() {
|
||||||
difference() {
|
difference() {
|
||||||
union() {
|
union() {
|
||||||
gridfinityInit(gridx, gridy, height(gridz, gridz_define, style_lip, enable_zsnap), 0, length, sl=style_lip)
|
gridfinityInit(gridx, gridy, height_mm, 0, length, sl=style_lip)
|
||||||
children();
|
children();
|
||||||
gridfinityBase(gridx, gridy, length, div_base_x, div_base_y, style_hole, only_corners=only_corners);
|
gridfinityBase(gridx, gridy, length, div_base_x, div_base_y, style_hole, only_corners=only_corners);
|
||||||
}
|
}
|
||||||
|
@ -100,7 +115,7 @@ module gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap
|
||||||
difference() {
|
difference() {
|
||||||
union() {
|
union() {
|
||||||
|
|
||||||
gridfinityInit(gridx, gridy, height(gridz, gridz_define, style_lip, enable_zsnap), 0, length, sl=style_lip)
|
gridfinityInit(gridx, gridy, height_mm, 0, length, sl=style_lip)
|
||||||
children();
|
children();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
include <standard.scad>
|
include <standard.scad>
|
||||||
use <generic-helpers.scad>
|
use <generic-helpers.scad>
|
||||||
|
use <gridfinity-rebuilt-holes.scad>
|
||||||
|
|
||||||
// ===== User Modules ===== //
|
// ===== 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=bundle_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];
|
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];
|
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));
|
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])
|
translate([0,0,-1])
|
||||||
rounded_rectangle(xx+0.005, yy+0.005, h_base+h_bot/2*10, r_fo1+0.001);
|
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(){
|
difference(){
|
||||||
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
|
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
|
||||||
block_base(gx, gy, l, dbnx, dbny, 0, off);
|
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])
|
copy_mirror([0, 1, 0]) {
|
||||||
refined_hole();
|
copy_mirror([1, 0, 0]) {
|
||||||
mirror([1, 0, 0])
|
translate([
|
||||||
translate([(gx/2)*l_grid - d_hole_from_side, (gy/2) * l_grid - d_hole_from_side, h_slit*2])
|
(gx/2)*l_grid - d_hole_from_side,
|
||||||
refined_hole();
|
(gy/2) * l_grid - d_hole_from_side,
|
||||||
mirror([0, 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();
|
block_base_hole(hole_options, off);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
pattern_linear(2, 2, (gx-1)*l_grid+d_hole, (gy-1)*l_grid+d_hole)
|
|
||||||
block_base_hole(style_hole, off);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
|
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 gx
|
||||||
* @param gy
|
* @param gy
|
||||||
* @param l
|
* @param l
|
||||||
* @param dbnx
|
* @param dbnx
|
||||||
* @param dbny
|
* @param dbny
|
||||||
* @param style_hole
|
* @param hole_options @see block_base_hole.hole_options
|
||||||
* @param off
|
* @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)
|
render(convexity = 2)
|
||||||
difference() {
|
difference() {
|
||||||
block_base_solid(dbnx, dbny, l, off);
|
block_base_solid(dbnx, dbny, l, off);
|
||||||
|
|
||||||
if (style_hole > 0)
|
pattern_circular(abs(l-d_hole_from_side/2)<0.001?1:4)
|
||||||
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])
|
||||||
if (style_hole == 4)
|
block_base_hole(hole_options, off);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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/
|
* @brief Stacking lip based on https://gridfinity.xyz/specification/
|
||||||
* @details Also includes a support base.
|
* @details Also includes a support base.
|
||||||
|
|
|
@ -15,7 +15,7 @@ $fa = 8;
|
||||||
$fs = 0.25;
|
$fs = 0.25;
|
||||||
|
|
||||||
/* [Bin or Base] */
|
/* [Bin or Base] */
|
||||||
type = 0; // [0:bin, 1:base]
|
type = 1; // [0:bin, 1:base]
|
||||||
|
|
||||||
/* [Printer Settings] */
|
/* [Printer Settings] */
|
||||||
// extrusion width (walls will be twice this size)
|
// extrusion width (walls will be twice this size)
|
||||||
|
@ -202,15 +202,17 @@ module gridfinityBaseVase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
module block_magnet_blank(o = 0, half = true) {
|
module block_magnet_blank(o = 0, half = true) {
|
||||||
|
magnet_radius = MAGNET_HOLE_RADIUS + o;
|
||||||
|
|
||||||
translate([d_hole/2,d_hole/2,-h_base+0.1])
|
translate([d_hole/2,d_hole/2,-h_base+0.1])
|
||||||
difference() {
|
difference() {
|
||||||
hull() {
|
hull() {
|
||||||
cylinder(r = r_hole2+o, h = h_hole*2, center = true);
|
cylinder(r = magnet_radius, h = MAGNET_HOLE_DEPTH*2, center = true);
|
||||||
cylinder(r = (r_hole2+o)-(h_base+0.1-h_hole), h = (h_base+0.1)*2, center = true);
|
cylinder(r = magnet_radius-(h_base+0.1-MAGNET_HOLE_DEPTH), h = (h_base+0.1)*2, center = true);
|
||||||
}
|
}
|
||||||
if (half)
|
if (half)
|
||||||
mirror([0,0,1])
|
mirror([0,0,1])
|
||||||
cylinder(r=(r_hole2+o)*2, h = (h_base+0.1)*4);
|
cylinder(r=magnet_radius*2, h = (h_base+0.1)*4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
images/base_hole_options/magnet_and_screw_holes_all.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/magnet_and_screw_holes_plain.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/magnet_and_screw_holes_printable.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/magnet_holes_chamfered.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/magnet_holes_plain.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/magnet_holes_printable.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/magnet_holes_with_crush_ribs.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/no_holes.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/refined_and_screw_holes.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
images/base_hole_options/refined_holes.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
images/base_hole_options/screw_holes_plain.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/base_hole_options/screw_holes_printable.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
images/hole_cutouts/all_hole_options.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
images/hole_cutouts/chamfered_magnet_hole.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
images/hole_cutouts/magnet_and_screw_hole.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
images/hole_cutouts/magnet_and_screw_hole_supportless.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
images/hole_cutouts/magnet_hole.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
images/hole_cutouts/magnet_hole_crush_ribs.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
images/hole_cutouts/magnet_hole_supportless.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
images/hole_cutouts/no_hole.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
images/hole_cutouts/refined_hole.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
images/hole_cutouts/screw_hole.png
Normal file
After Width: | Height: | Size: 14 KiB |
|
@ -21,18 +21,48 @@ l_grid = 42;
|
||||||
// Per spec, matches radius of upper base section.
|
// Per spec, matches radius of upper base section.
|
||||||
r_base = r_fo1;
|
r_base = r_fo1;
|
||||||
|
|
||||||
// screw hole radius
|
// Tollerance to make sure cuts don't leave a sliver behind,
|
||||||
r_hole1 = 1.5;
|
// and that items are properly connected to each other.
|
||||||
// magnet hole radius
|
TOLLERANCE = 0.01;
|
||||||
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
|
// center-to-center distance between holes
|
||||||
d_hole = 26;
|
d_hole = 26;
|
||||||
// distance of hole from side of bin
|
// distance of hole from side of bin
|
||||||
d_hole_from_side=8;
|
d_hole_from_side=8;
|
||||||
// magnet hole depth
|
|
||||||
h_hole = 2.4;
|
// Meassured diameter in Fusion360.
|
||||||
// slit depth (printer layer height)
|
// Smaller than the magnet to keep it squeezed.
|
||||||
h_slit = 0.2;
|
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;
|
||||||
|
// ****************************************
|
||||||
|
|
||||||
// top edge fillet radius
|
// top edge fillet radius
|
||||||
r_f1 = 0.6;
|
r_f1 = 0.6;
|
||||||
|
@ -93,13 +123,7 @@ bp_rcut_length = 4.25;
|
||||||
bp_rcut_depth = 2;
|
bp_rcut_depth = 2;
|
||||||
// Baseplate clearance offset
|
// Baseplate clearance offset
|
||||||
bp_xy_clearance = 0.5;
|
bp_xy_clearance = 0.5;
|
||||||
// countersink diameter for baseplate
|
|
||||||
d_cs = 2.5;
|
|
||||||
// radius of cutout for skeletonized baseplate
|
// radius of cutout for skeletonized baseplate
|
||||||
r_skel = 2;
|
r_skel = 2;
|
||||||
// baseplate counterbore radius
|
|
||||||
r_cb = 2.75;
|
|
||||||
// baseplate counterbore depth
|
|
||||||
h_cb = 3;
|
|
||||||
// minimum baseplate thickness (when skeletonized)
|
// minimum baseplate thickness (when skeletonized)
|
||||||
h_skel = 1;
|
h_skel = 1;
|
||||||
|
|
24
tests/gridfinity-rebuilt-baseplate.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"fileFormatVersion": "1",
|
||||||
|
"parameterSets": {
|
||||||
|
"Default": {
|
||||||
|
"$fa": "8",
|
||||||
|
"$fs": "0.25",
|
||||||
|
"d_screw": "3.3500000000000001",
|
||||||
|
"d_screw_head": "5",
|
||||||
|
"distancex": "0",
|
||||||
|
"distancey": "0",
|
||||||
|
"chamfer_holes": "true",
|
||||||
|
"crush_ribs": "true",
|
||||||
|
"enable_magnet": "true",
|
||||||
|
"fitx": "0",
|
||||||
|
"fity": "0",
|
||||||
|
"gridx": "1",
|
||||||
|
"gridy": "1",
|
||||||
|
"n_screws": "1",
|
||||||
|
"screw_spacing": "0.5",
|
||||||
|
"style_hole": "1",
|
||||||
|
"style_plate": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
tests/gridfinity-rebuilt-bins.json
Normal 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_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_holes": "true",
|
||||||
|
"scoop": "0",
|
||||||
|
"screw_holes": "false",
|
||||||
|
"style_lip": "0",
|
||||||
|
"style_tab": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
tests/gridfinity-spiral-vase.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"fileFormatVersion": "1",
|
||||||
|
"parameterSets": {
|
||||||
|
"Default": {
|
||||||
|
"$fa": "8",
|
||||||
|
"$fs": "0.25",
|
||||||
|
"a_tab": "40",
|
||||||
|
"bottom_layer": "3",
|
||||||
|
"enable_funnel": "true",
|
||||||
|
"enable_holes": "true",
|
||||||
|
"enable_inset": "true",
|
||||||
|
"enable_lip": "true",
|
||||||
|
"enable_pinch": "true",
|
||||||
|
"enable_scoop_chamfer": "true",
|
||||||
|
"enable_zsnap": "false",
|
||||||
|
"gridx": "1",
|
||||||
|
"gridy": "1",
|
||||||
|
"gridz": "6",
|
||||||
|
"gridz_define": "0",
|
||||||
|
"layer": "0.35",
|
||||||
|
"n_divx": "2",
|
||||||
|
"nozzle": "0.6",
|
||||||
|
"style_base": "0",
|
||||||
|
"style_tab": "0",
|
||||||
|
"type": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
tests/openscad_runner.py
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
"""
|
||||||
|
Helpful classes for running OpenScad from Python.
|
||||||
|
@Copyright Arthur Moore 2024 MIT License
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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(frozen=True)
|
||||||
|
class CameraArguments:
|
||||||
|
"""
|
||||||
|
Controls the camera position when outputting to png format.
|
||||||
|
@see `openscad -h`.
|
||||||
|
Supports fluid interface.
|
||||||
|
"""
|
||||||
|
translate: Vec3
|
||||||
|
rotate: Vec3
|
||||||
|
distance: float
|
||||||
|
|
||||||
|
def with_translation(self, new_translate: Vec3) -> CameraArguments:
|
||||||
|
return CameraArguments(translate=new_translate, rotate=self.rotate, distance=self.distance)
|
||||||
|
|
||||||
|
def with_rotation(self, new_rotate: Vec3) -> CameraArguments:
|
||||||
|
return CameraArguments(translate=self.translate, rotate=new_rotate, distance=self.distance)
|
||||||
|
|
||||||
|
def with_distance(self, new_distance: float) -> CameraArguments:
|
||||||
|
return CameraArguments(translate=self.translate, rotate=rotate, distance=new_distance)
|
||||||
|
|
||||||
|
def as_argument(self) -> str:
|
||||||
|
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 CameraRotations:
|
||||||
|
'''Pre-defined useful camera rotations'''
|
||||||
|
Default = Vec3(0,0,0),
|
||||||
|
AngledTop = Vec3(45,0,45)
|
||||||
|
AngledBottom = Vec3(225,0,225)
|
||||||
|
Top = Vec3(45,0,0)
|
||||||
|
|
||||||
|
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),150)
|
||||||
|
|
||||||
|
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.camera_arguments = None
|
||||||
|
self.parameters = None
|
||||||
|
|
||||||
|
def create_image(self, 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 + \
|
||||||
|
([self.camera_arguments.as_argument()] if self.camera_arguments != None else []) + \
|
||||||
|
args + \
|
||||||
|
["-o", str(image_path), str(self.scad_file_path)]
|
||||||
|
#print(command_arguments)
|
||||||
|
|
||||||
|
if self.parameters != None:
|
||||||
|
#print(self.parameters)
|
||||||
|
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)
|
116
tests/test_baseplate.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
Tests for gridfinity-rebuilt-baseplate.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 TestBasePlateHoles(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test creating a single base in "gridfinity-spiral-vase.scad"
|
||||||
|
|
||||||
|
Currently only makes sure code runs, and outputs pictures for manual verification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
parameter_file_path = Path("gridfinity-rebuilt-baseplate.json")
|
||||||
|
parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text())
|
||||||
|
cls.default_parameters = parameter_file_data.parameterSets["Default"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-baseplate.scad'))
|
||||||
|
self.scad_runner.image_folder_base = Path('../images/baseplate/')
|
||||||
|
self.scad_runner.parameters = self.default_parameters.copy()
|
||||||
|
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150)
|
||||||
|
|
||||||
|
def test_no_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = False
|
||||||
|
vars["style_hole"] = 0
|
||||||
|
self.scad_runner.create_image([], Path('no_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('no_holes_top.png'))
|
||||||
|
|
||||||
|
def test_plain_magnet_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = True
|
||||||
|
vars["style_hole"] = 0
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
self.scad_runner.create_image([], Path('magnet_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('plain_magnet_holes_top.png'))
|
||||||
|
|
||||||
|
def test_chamfered_magnet_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = True
|
||||||
|
vars["style_hole"] = 0
|
||||||
|
vars["chamfer_holes"] = True
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('chamfered_magnet_holes.png'))
|
||||||
|
|
||||||
|
def test_ribbed_magnet_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = True
|
||||||
|
vars["style_hole"] = 0
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["crush_ribs"] = True
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('ribbed_magnet_holes.png'))
|
||||||
|
|
||||||
|
def test_chamfered_and_ribbed_magnet_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = True
|
||||||
|
vars["style_hole"] = 0
|
||||||
|
vars["chamfer_holes"] = True
|
||||||
|
vars["crush_ribs"] = True
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('chamfered_and_ribbed_magnet_holes.png'))
|
||||||
|
|
||||||
|
def test_only_countersunk_screw_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = False
|
||||||
|
vars["style_hole"] = 1
|
||||||
|
self.scad_runner.create_image([], Path('only_countersunk_screw_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('only_countersunk_screw_holes_top.png'))
|
||||||
|
|
||||||
|
def test_only_counterbored_screw_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = False
|
||||||
|
vars["style_hole"] = 2
|
||||||
|
self.scad_runner.create_image([], Path('only_counterbored_screw_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('only_counterbored_screw_holes_top.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_countersunk_screw_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = True
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["style_hole"] = 1
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_countersunk_screw_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_countersunk_screw_holes_top.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_counterbored_screw_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_magnet"] = True
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["style_hole"] = 2
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_counterbored_screw_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_counterbored_screw_holes_top.png'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
142
tests/test_bins.py
Normal 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"]
|
||||||
|
|
||||||
|
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()
|
||||||
|
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150)
|
||||||
|
|
||||||
|
def test_no_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = False
|
||||||
|
vars["screw_holes"] = False
|
||||||
|
self.scad_runner.create_image([], Path('no_holes.png'))
|
||||||
|
|
||||||
|
def test_refined_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = True
|
||||||
|
vars["magnet_holes"] = False
|
||||||
|
vars["screw_holes"] = False
|
||||||
|
self.scad_runner.create_image([], Path('refined_holes.png'))
|
||||||
|
|
||||||
|
def test_refined_and_screw_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = True
|
||||||
|
vars["magnet_holes"] = False
|
||||||
|
vars["screw_holes"] = True
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image([], Path('refined_and_screw_holes.png'))
|
||||||
|
|
||||||
|
def test_screw_holes_plain(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = False
|
||||||
|
vars["screw_holes"] = True
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image([], Path('screw_holes_plain.png'))
|
||||||
|
|
||||||
|
def test_screw_holes_printable(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = False
|
||||||
|
vars["screw_holes"] = True
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image([], Path('screw_holes_printable.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_plain(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image([], Path('magnet_holes_plain.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_chamfered(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_holes"] = True
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image([], Path('magnet_holes_chamfered.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_printable(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = False
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image([], Path('magnet_holes_printable.png'))
|
||||||
|
|
||||||
|
def test_magnet_holes_with_crush_ribs(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = False
|
||||||
|
vars["crush_ribs"] = True
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image([], Path('magnet_holes_with_crush_ribs.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_screw_holes_plain(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = True
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["printable_hole_top"] = False
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_screw_holes_plain.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_screw_holes_printable(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = True
|
||||||
|
vars["crush_ribs"] = False
|
||||||
|
vars["chamfer_holes"] = False
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_screw_holes_printable.png'))
|
||||||
|
|
||||||
|
def test_magnet_and_screw_holes_all(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["refined_holes"] = False
|
||||||
|
vars["magnet_holes"] = True
|
||||||
|
vars["screw_holes"] = True
|
||||||
|
vars["crush_ribs"] = True
|
||||||
|
vars["chamfer_holes"] = True
|
||||||
|
vars["printable_hole_top"] = True
|
||||||
|
self.scad_runner.create_image([], Path('magnet_and_screw_holes_all.png'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
78
tests/test_holes.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-holes.scad'))
|
||||||
|
self.scad_runner.image_folder_base = Path('../images/hole_cutouts/')
|
||||||
|
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledTop, 50)
|
||||||
|
|
||||||
|
def test_refined_hole(self):
|
||||||
|
"""
|
||||||
|
refined_hole() is special, since top_angle_camera is not appropriate for it.
|
||||||
|
"""
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledBottom)
|
||||||
|
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(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(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(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(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(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(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(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(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(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(test_args, Path('no_hole.png'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
50
tests/test_spiral_vase.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"""
|
||||||
|
Tests for gridfinity-spiral-vase.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 TestSpiralVaseBase(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Test creating a single base in "gridfinity-spiral-vase.scad"
|
||||||
|
|
||||||
|
Currently only makes sure code runs, and outputs pictures for manual verification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
parameter_file_path = Path("gridfinity-spiral-vase.json")
|
||||||
|
parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text())
|
||||||
|
cls.default_parameters = parameter_file_data.parameterSets["Default"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.scad_runner = OpenScadRunner(Path('../gridfinity-spiral-vase.scad'))
|
||||||
|
self.scad_runner.image_folder_base = Path('../images/spiral_vase_base/')
|
||||||
|
self.scad_runner.parameters = self.default_parameters.copy()
|
||||||
|
self.scad_runner.parameters["type"] = 1 # Create a Base
|
||||||
|
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150)
|
||||||
|
|
||||||
|
def test_no_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_holes"] = False
|
||||||
|
self.scad_runner.create_image([], Path('no_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.Top)
|
||||||
|
self.scad_runner.create_image([], Path('no_holes_top.png'))
|
||||||
|
|
||||||
|
def test_refined_holes(self):
|
||||||
|
vars = self.scad_runner.parameters
|
||||||
|
vars["enable_holes"] = True
|
||||||
|
self.scad_runner.create_image([], Path('with_holes_bottom.png'))
|
||||||
|
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.Top)
|
||||||
|
self.scad_runner.create_image([], Path('with_holes_top.png'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|