diff --git a/.gitignore b/.gitignore index a3b8146..6a8861d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/generic-helpers.scad b/generic-helpers.scad index 99a96dc..43a32b7 100644 --- a/generic-helpers.scad +++ b/generic-helpers.scad @@ -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) diff --git a/gridfinity-rebuilt-baseplate.scad b/gridfinity-rebuilt-baseplate.scad index e806820..202dbcb 100644 --- a/gridfinity-rebuilt-baseplate.scad +++ b/gridfinity-rebuilt-baseplate.scad @@ -1,5 +1,6 @@ include include +use // ===== INFORMATION ===== // /* @@ -49,23 +50,29 @@ fity = 0; // [-1:0.1:1] // baseplate styles style_plate = 0; // [0: thin, 1:weighted, 2:skeletonized, 3: screw together, 4: screw together minimal] -// enable magnet hole -enable_magnet = true; // hole styles 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 ===== // -screw_together = (style_plate == 3 || style_plate == 4); 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 ===== // -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(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); 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; 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]) 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]) 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(){ - 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(); 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) = - screw_together - ? 6.75 - :sp==0 - ?0 - : sp==1 - ?bp_h_bot - :h_skel + (sm - ?h_hole - : 0)+(sh==0 - ? d_screw - : sh==1 - ?d_cs - :h_cb); +function calculate_offset(style_plate, enable_magnet, style_hole) = + assert(style_plate >=0 && style_plate <=4) + let (screw_together = style_plate == 3 || style_plate == 4) + screw_together ? 6.75 : + style_plate==0 ? 0 : + style_plate==1 ? bp_h_bot : + calculate_offset_skeletonized(enable_magnet, style_hole); + +function calculate_offset_skeletonized(enable_magnet, style_hole) = + h_skel + (enable_magnet ? MAGNET_HOLE_DEPTH : 0) + + ( + style_hole==0 ? d_screw : + style_hole==1 ? BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS : // Only works because countersink is at 45 degree angle! + BASEPLATE_SCREW_COUNTERBORE_HEIGHT + ); module cutter_weight() { union() { @@ -156,23 +166,19 @@ module hole_pattern(){ } module cutter_countersink(){ - cylinder(r = r_hole1+d_clear, h = 100*h_base, center = true); - translate([0,0,d_cs]) - 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); - } + screw_hole(SCREW_HOLE_RADIUS + d_clear, 2*h_base, + false, BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS); } module cutter_counterbore(){ - cylinder(h=100*h_base, r=r_hole1+d_clear, center=true); - difference() { - cylinder(h = 2*(h_cb+0.2), r=r_cb, center=true); - copy_mirror([0,1,0]) - translate([-1.5*r_cb,r_hole1+d_clear+0.1,h_cb-h_slit]) - cube([r_cb*3,r_cb*3, 10]); + screw_radius = SCREW_HOLE_RADIUS + d_clear; + counterbore_height = BASEPLATE_SCREW_COUNTERBORE_HEIGHT + 2*LAYER_HEIGHT; + union(){ + cylinder(h=2*h_base, r=screw_radius); + difference() { + 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]) minkowski() { square([l,l]); - circle(r_hole2+r_skel+2); + circle(MAGNET_HOLE_RADIUS+r_skel+2); } } circle(r_skel); diff --git a/gridfinity-rebuilt-bins.scad b/gridfinity-rebuilt-bins.scad index ea176a9..8e908b1 100644 --- a/gridfinity-rebuilt-bins.scad +++ b/gridfinity-rebuilt-bins.scad @@ -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_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 ===== // @@ -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); } diff --git a/gridfinity-rebuilt-holes.scad b/gridfinity-rebuilt-holes.scad new file mode 100644 index 0000000..382cd05 --- /dev/null +++ b/gridfinity-rebuilt-holes.scad @@ -0,0 +1,316 @@ +/** + * @file gridfinity-rebuilt-holes.scad + * @brief Functions to create different types of holes in an object. + */ + +include +use + +/** + * @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); diff --git a/gridfinity-rebuilt-lite.scad b/gridfinity-rebuilt-lite.scad index 4f5195c..da3e7df 100644 --- a/gridfinity-rebuilt-lite.scad +++ b/gridfinity-rebuilt-lite.scad @@ -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] /* [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) 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) @@ -51,22 +48,40 @@ div_base_y = 0; // thickness of bottom layer 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 ===== // // Input all the cutter types in here 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); } // ===== CONSTRUCTION ===== // 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() { difference() { 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(); 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() { 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(); } diff --git a/gridfinity-rebuilt-utility.scad b/gridfinity-rebuilt-utility.scad index 6404080..80f28e2 100644 --- a/gridfinity-rebuilt-utility.scad +++ b/gridfinity-rebuilt-utility.scad @@ -6,6 +6,7 @@ include use +use // ===== 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]; 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. diff --git a/gridfinity-spiral-vase.scad b/gridfinity-spiral-vase.scad index ee11b1a..e03ebd7 100644 --- a/gridfinity-spiral-vase.scad +++ b/gridfinity-spiral-vase.scad @@ -15,7 +15,7 @@ $fa = 8; $fs = 0.25; /* [Bin or Base] */ -type = 0; // [0:bin, 1:base] +type = 1; // [0:bin, 1:base] /* [Printer Settings] */ // extrusion width (walls will be twice this size) @@ -202,15 +202,17 @@ module gridfinityBaseVase() { } 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]) difference() { hull() { - cylinder(r = r_hole2+o, h = h_hole*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 = MAGNET_HOLE_DEPTH*2, center = true); + cylinder(r = magnet_radius-(h_base+0.1-MAGNET_HOLE_DEPTH), h = (h_base+0.1)*2, center = true); } if (half) 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); } } diff --git a/images/base_hole_options/magnet_and_screw_holes_all.png b/images/base_hole_options/magnet_and_screw_holes_all.png new file mode 100644 index 0000000..72e34b9 Binary files /dev/null and b/images/base_hole_options/magnet_and_screw_holes_all.png differ diff --git a/images/base_hole_options/magnet_and_screw_holes_plain.png b/images/base_hole_options/magnet_and_screw_holes_plain.png new file mode 100644 index 0000000..72e34b9 Binary files /dev/null and b/images/base_hole_options/magnet_and_screw_holes_plain.png differ diff --git a/images/base_hole_options/magnet_and_screw_holes_printable.png b/images/base_hole_options/magnet_and_screw_holes_printable.png new file mode 100644 index 0000000..72e34b9 Binary files /dev/null and b/images/base_hole_options/magnet_and_screw_holes_printable.png differ diff --git a/images/base_hole_options/magnet_holes_chamfered.png b/images/base_hole_options/magnet_holes_chamfered.png new file mode 100644 index 0000000..b82e0cf Binary files /dev/null and b/images/base_hole_options/magnet_holes_chamfered.png differ diff --git a/images/base_hole_options/magnet_holes_plain.png b/images/base_hole_options/magnet_holes_plain.png new file mode 100644 index 0000000..b82e0cf Binary files /dev/null and b/images/base_hole_options/magnet_holes_plain.png differ diff --git a/images/base_hole_options/magnet_holes_printable.png b/images/base_hole_options/magnet_holes_printable.png new file mode 100644 index 0000000..b82e0cf Binary files /dev/null and b/images/base_hole_options/magnet_holes_printable.png differ diff --git a/images/base_hole_options/magnet_holes_with_crush_ribs.png b/images/base_hole_options/magnet_holes_with_crush_ribs.png new file mode 100644 index 0000000..72e34b9 Binary files /dev/null and b/images/base_hole_options/magnet_holes_with_crush_ribs.png differ diff --git a/images/base_hole_options/no_holes.png b/images/base_hole_options/no_holes.png new file mode 100644 index 0000000..b82e0cf Binary files /dev/null and b/images/base_hole_options/no_holes.png differ diff --git a/images/base_hole_options/refined_and_screw_holes.png b/images/base_hole_options/refined_and_screw_holes.png new file mode 100644 index 0000000..95e1638 Binary files /dev/null and b/images/base_hole_options/refined_and_screw_holes.png differ diff --git a/images/base_hole_options/refined_holes.png b/images/base_hole_options/refined_holes.png new file mode 100644 index 0000000..95e1638 Binary files /dev/null and b/images/base_hole_options/refined_holes.png differ diff --git a/images/base_hole_options/screw_holes_plain.png b/images/base_hole_options/screw_holes_plain.png new file mode 100644 index 0000000..b82e0cf Binary files /dev/null and b/images/base_hole_options/screw_holes_plain.png differ diff --git a/images/base_hole_options/screw_holes_printable.png b/images/base_hole_options/screw_holes_printable.png new file mode 100644 index 0000000..b82e0cf Binary files /dev/null and b/images/base_hole_options/screw_holes_printable.png differ diff --git a/images/hole_cutouts/all_hole_options.png b/images/hole_cutouts/all_hole_options.png new file mode 100644 index 0000000..1c66780 Binary files /dev/null and b/images/hole_cutouts/all_hole_options.png differ diff --git a/images/hole_cutouts/chamfered_magnet_hole.png b/images/hole_cutouts/chamfered_magnet_hole.png new file mode 100644 index 0000000..e37a0ac Binary files /dev/null and b/images/hole_cutouts/chamfered_magnet_hole.png differ diff --git a/images/hole_cutouts/magnet_and_screw_hole.png b/images/hole_cutouts/magnet_and_screw_hole.png new file mode 100644 index 0000000..05fb4ad Binary files /dev/null and b/images/hole_cutouts/magnet_and_screw_hole.png differ diff --git a/images/hole_cutouts/magnet_and_screw_hole_supportless.png b/images/hole_cutouts/magnet_and_screw_hole_supportless.png new file mode 100644 index 0000000..3e45f2c Binary files /dev/null and b/images/hole_cutouts/magnet_and_screw_hole_supportless.png differ diff --git a/images/hole_cutouts/magnet_hole.png b/images/hole_cutouts/magnet_hole.png new file mode 100644 index 0000000..72c5ab8 Binary files /dev/null and b/images/hole_cutouts/magnet_hole.png differ diff --git a/images/hole_cutouts/magnet_hole_crush_ribs.png b/images/hole_cutouts/magnet_hole_crush_ribs.png new file mode 100644 index 0000000..652ed99 Binary files /dev/null and b/images/hole_cutouts/magnet_hole_crush_ribs.png differ diff --git a/images/hole_cutouts/magnet_hole_supportless.png b/images/hole_cutouts/magnet_hole_supportless.png new file mode 100644 index 0000000..0e3b951 Binary files /dev/null and b/images/hole_cutouts/magnet_hole_supportless.png differ diff --git a/images/hole_cutouts/no_hole.png b/images/hole_cutouts/no_hole.png new file mode 100644 index 0000000..fc39d3a Binary files /dev/null and b/images/hole_cutouts/no_hole.png differ diff --git a/images/hole_cutouts/refined_hole.png b/images/hole_cutouts/refined_hole.png new file mode 100644 index 0000000..477b160 Binary files /dev/null and b/images/hole_cutouts/refined_hole.png differ diff --git a/images/hole_cutouts/screw_hole.png b/images/hole_cutouts/screw_hole.png new file mode 100644 index 0000000..a31d1ab Binary files /dev/null and b/images/hole_cutouts/screw_hole.png differ diff --git a/standard.scad b/standard.scad index 5da1046..1ceb504 100644 --- a/standard.scad +++ b/standard.scad @@ -21,18 +21,48 @@ 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; +// Tollerance to make sure cuts don't leave a sliver behind, +// and that items are properly connected to each other. +TOLLERANCE = 0.01; + +// **************************************** +// 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; + +// 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 r_f1 = 0.6; @@ -93,13 +123,7 @@ bp_rcut_length = 4.25; bp_rcut_depth = 2; // Baseplate clearance offset bp_xy_clearance = 0.5; -// countersink diameter for baseplate -d_cs = 2.5; // radius of cutout for skeletonized baseplate r_skel = 2; -// baseplate counterbore radius -r_cb = 2.75; -// baseplate counterbore depth -h_cb = 3; // minimum baseplate thickness (when skeletonized) h_skel = 1; diff --git a/tests/gridfinity-rebuilt-baseplate.json b/tests/gridfinity-rebuilt-baseplate.json new file mode 100644 index 0000000..ee97ef5 --- /dev/null +++ b/tests/gridfinity-rebuilt-baseplate.json @@ -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" + } + } +} diff --git a/tests/gridfinity-rebuilt-bins.json b/tests/gridfinity-rebuilt-bins.json new file mode 100644 index 0000000..3d93ee5 --- /dev/null +++ b/tests/gridfinity-rebuilt-bins.json @@ -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" + } + } +} diff --git a/tests/gridfinity-spiral-vase.json b/tests/gridfinity-spiral-vase.json new file mode 100644 index 0000000..3aa2bad --- /dev/null +++ b/tests/gridfinity-spiral-vase.json @@ -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" + } + } +} diff --git a/tests/openscad_runner.py b/tests/openscad_runner.py new file mode 100644 index 0000000..c73987d --- /dev/null +++ b/tests/openscad_runner.py @@ -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) diff --git a/tests/test_baseplate.py b/tests/test_baseplate.py new file mode 100644 index 0000000..9984310 --- /dev/null +++ b/tests/test_baseplate.py @@ -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() diff --git a/tests/test_bins.py b/tests/test_bins.py new file mode 100644 index 0000000..1c922b0 --- /dev/null +++ b/tests/test_bins.py @@ -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() diff --git a/tests/test_holes.py b/tests/test_holes.py new file mode 100644 index 0000000..70e9bbf --- /dev/null +++ b/tests/test_holes.py @@ -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() diff --git a/tests/test_spiral_vase.py b/tests/test_spiral_vase.py new file mode 100644 index 0000000..8c7d84a --- /dev/null +++ b/tests/test_spiral_vase.py @@ -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()