diff --git a/gridfinity-rebuilt-bins.scad b/gridfinity-rebuilt-bins.scad index 929e5436..c3a813af 100644 --- a/gridfinity-rebuilt-bins.scad +++ b/gridfinity-rebuilt-bins.scad @@ -101,6 +101,8 @@ chamfer_holes = true; printable_hole_top = true; // Enable "gridfinity-refined" thumbscrew hole in the center of each base: https://www.printables.com/model/413761-gridfinity-refined enable_thumbscrew = false; +// Where to place the bases +base_locations = 1; // [0: All bases, 1: Corners only, 2: Edges only] hole_options = bundle_hole_options(refined_holes, magnet_holes, screw_holes, crush_ribs, chamfer_holes, printable_hole_top); grid_dimensions = GRID_DIMENSIONS_MM / (half_grid ? 2 : 1); @@ -119,7 +121,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], grid_dimensions=grid_dimensions, hole_options=hole_options, only_corners=only_corners || half_grid, thumbscrew=enable_thumbscrew); +gridfinityBase([gridx, gridy], grid_dimensions=grid_dimensions, hole_options=hole_options, only_corners=only_corners || half_grid, thumbscrew=enable_thumbscrew, base_locations=base_locations); //} diff --git a/images/base_hole_options/base_locations_all_bases.png b/images/base_hole_options/base_locations_all_bases.png new file mode 100644 index 00000000..73b5275f Binary files /dev/null and b/images/base_hole_options/base_locations_all_bases.png differ diff --git a/images/base_hole_options/base_locations_corners_only.png b/images/base_hole_options/base_locations_corners_only.png new file mode 100644 index 00000000..c99a9317 Binary files /dev/null and b/images/base_hole_options/base_locations_corners_only.png differ diff --git a/images/base_hole_options/base_locations_edges_only.png b/images/base_hole_options/base_locations_edges_only.png new file mode 100644 index 00000000..aa8b0abf Binary files /dev/null and b/images/base_hole_options/base_locations_edges_only.png differ diff --git a/images/base_hole_options/base_locations_large_grid_all_bases.png b/images/base_hole_options/base_locations_large_grid_all_bases.png new file mode 100644 index 00000000..505b8baf Binary files /dev/null and b/images/base_hole_options/base_locations_large_grid_all_bases.png differ diff --git a/images/base_hole_options/base_locations_large_grid_corners_only.png b/images/base_hole_options/base_locations_large_grid_corners_only.png new file mode 100644 index 00000000..441075dc Binary files /dev/null and b/images/base_hole_options/base_locations_large_grid_corners_only.png differ diff --git a/images/base_hole_options/base_locations_large_grid_edges_only.png b/images/base_hole_options/base_locations_large_grid_edges_only.png new file mode 100644 index 00000000..ebd7cf51 Binary files /dev/null and b/images/base_hole_options/base_locations_large_grid_edges_only.png differ diff --git a/images/base_hole_options/magnet_and_screw_holes_all.png b/images/base_hole_options/magnet_and_screw_holes_all.png index 72e34b97..d5d73989 100644 Binary files a/images/base_hole_options/magnet_and_screw_holes_all.png 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 index 72e34b97..db5de899 100644 Binary files a/images/base_hole_options/magnet_and_screw_holes_plain.png 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 index 72e34b97..77c8990a 100644 Binary files a/images/base_hole_options/magnet_and_screw_holes_printable.png 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 index b82e0cf3..44e3237e 100644 Binary files a/images/base_hole_options/magnet_holes_chamfered.png 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 index b82e0cf3..3cd8333f 100644 Binary files a/images/base_hole_options/magnet_holes_plain.png 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 index b82e0cf3..debca558 100644 Binary files a/images/base_hole_options/magnet_holes_printable.png 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 index 72e34b97..3778944a 100644 Binary files a/images/base_hole_options/magnet_holes_with_crush_ribs.png and b/images/base_hole_options/magnet_holes_with_crush_ribs.png differ diff --git a/images/base_hole_options/refined_and_screw_holes.png b/images/base_hole_options/refined_and_screw_holes.png index 95e1638b..87d3c26f 100644 Binary files a/images/base_hole_options/refined_and_screw_holes.png 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 index 95e1638b..c2fcac87 100644 Binary files a/images/base_hole_options/refined_holes.png 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 index b82e0cf3..334b8eea 100644 Binary files a/images/base_hole_options/screw_holes_plain.png 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 index b82e0cf3..f6c689dc 100644 Binary files a/images/base_hole_options/screw_holes_printable.png and b/images/base_hole_options/screw_holes_printable.png differ diff --git a/src/core/gridfinity-rebuilt-utility.scad b/src/core/gridfinity-rebuilt-utility.scad index ff7d4536..325446f5 100644 --- a/src/core/gridfinity-rebuilt-utility.scad +++ b/src/core/gridfinity-rebuilt-utility.scad @@ -240,13 +240,103 @@ module cut_move(x, y, w, h) { // ===== Modules ===== // +/** + * @brief Internal module to handle base pattern generation based on location options + */ +module _pattern_bases(grid_size, grid_dimensions, individual_base_size_mm, thumbscrew, base_locations, hole_options, off, base_solid=true) { + if(base_locations == 0) { + // All bases + pattern_linear(grid_size.x, grid_size.y, grid_dimensions.x, grid_dimensions.y) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + } else if(base_locations == 1) { + // Corners only + // Bottom left + translate([-grid_dimensions.x * (grid_size.x-1)/2, -grid_dimensions.y * (grid_size.y-1)/2, 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + + // Bottom right + translate([grid_dimensions.x * (grid_size.x-1)/2, -grid_dimensions.y * (grid_size.y-1)/2, 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + + // Top left + translate([-grid_dimensions.x * (grid_size.x-1)/2, grid_dimensions.y * (grid_size.y-1)/2, 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + + // Top right + translate([grid_dimensions.x * (grid_size.x-1)/2, grid_dimensions.y * (grid_size.y-1)/2, 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + } else if(base_locations == 2) { + // Edges only + // Bottom edge + for(x = [0:grid_size.x-1]) { + translate([grid_dimensions.x * (x - (grid_size.x-1)/2), -grid_dimensions.y * (grid_size.y-1)/2, 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + } + + // Top edge + for(x = [0:grid_size.x-1]) { + translate([grid_dimensions.x * (x - (grid_size.x-1)/2), grid_dimensions.y * (grid_size.y-1)/2, 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + } + + // Left edge (excluding corners which are already done) + for(y = [1:grid_size.y-2]) { + translate([-grid_dimensions.x * (grid_size.x-1)/2, grid_dimensions.y * (y - (grid_size.y-1)/2), 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + } + + // Right edge (excluding corners which are already done) + for(y = [1:grid_size.y-2]) { + translate([grid_dimensions.x * (grid_size.x-1)/2, grid_dimensions.y * (y - (grid_size.y-1)/2), 0]) + if (solid_base) { + base_solid(individual_base_size_mm); + } else { + block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } + } + } +} + /** * @brief Create the base of a gridfinity bin, or use it for a custom object. * @param grid_size Number of bases in each dimension. [x, y] * @param grid_dimensions [length, width] of a single Gridfinity base. * @param thumbscrew Enable "gridfinity-refined" thumbscrew hole in the center of each base unit. This is a ISO Metric Profile, 15.0mm size, M15x1.5 designation. + * @param base_locations Where to place the bases. [0: All bases, 1: Corners only, 2: Edges only] */ -module gridfinityBase(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, hole_options=bundle_hole_options(), off=0, final_cut=true, only_corners=false, thumbscrew=false) { +module gridfinityBase(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, hole_options=bundle_hole_options(), off=0, final_cut=true, only_corners=false, thumbscrew=false, base_locations=0) { assert(is_list(grid_dimensions) && len(grid_dimensions) == 2 && grid_dimensions.x > 0 && grid_dimensions.y > 0); assert(is_list(grid_size) && len(grid_size) == 2 && @@ -256,6 +346,7 @@ module gridfinityBase(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, hole_option is_bool(only_corners) && is_bool(thumbscrew) ); + assert(base_locations >= 0 && base_locations <= 2, "base_locations must be 0 (all), 1 (corners only), or 2 (edges only)"); // Per spec, there's a 0.5mm gap between each base. // This must be kept constant or half bins may not work correctly. @@ -277,10 +368,8 @@ module gridfinityBase(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, hole_option } if(only_corners) { - difference(){ - pattern_linear(grid_size.x, grid_size.y, grid_dimensions.x, grid_dimensions.y) { - base_solid(individual_base_size_mm); - } + difference() { + _pattern_bases(grid_size, grid_dimensions, individual_base_size_mm, thumbscrew, base_locations, hole_options, off, base_solid=true); if(thumbscrew) { thumbscrew_position = grid_size_mm - individual_base_size_mm; @@ -292,10 +381,8 @@ module gridfinityBase(grid_size, grid_dimensions=GRID_DIMENSIONS_MM, hole_option _base_holes(hole_options, off, grid_size_mm); _base_preview_fix(); } - } - else { - pattern_linear(grid_size.x, grid_size.y, grid_dimensions.x, grid_dimensions.y) - block_base(hole_options, off, individual_base_size_mm, thumbscrew=thumbscrew); + } else { + _pattern_bases(grid_size, grid_dimensions, individual_base_size_mm, thumbscrew, base_locations, hole_options, off); } } diff --git a/tests/gridfinity-rebuilt-bins.json b/tests/gridfinity-rebuilt-bins.json index 3d93ee53..7eed8c63 100644 --- a/tests/gridfinity-rebuilt-bins.json +++ b/tests/gridfinity-rebuilt-bins.json @@ -30,7 +30,8 @@ "scoop": "0", "screw_holes": "false", "style_lip": "0", - "style_tab": "1" + "style_tab": "1", + "base_locations": "0" } } } diff --git a/tests/openscad_runner.py b/tests/openscad_runner.py index 3609e8e5..c187d2a6 100644 --- a/tests/openscad_runner.py +++ b/tests/openscad_runner.py @@ -6,6 +6,7 @@ import json import subprocess +import platform from dataclasses import dataclass, is_dataclass, asdict from pathlib import Path from tempfile import NamedTemporaryFile @@ -105,6 +106,7 @@ class OpenScadRunner: parameters: Optional[dict] '''If set, a temporary parameter file is created, and used with these variables''' + MACOS_DEFAULT_PATH = '/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD' WINDOWS_DEFAULT_PATH = 'C:\\Program Files\\OpenSCAD\\openscad.exe' TOP_ANGLE_CAMERA = CameraArguments(Vec3(0,0,0),Vec3(45,0,45),150) @@ -120,7 +122,7 @@ class OpenScadRunner: 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.openscad_binary_path = self.MACOS_DEFAULT_PATH if platform.system() == 'Darwin' else self.WINDOWS_DEFAULT_PATH self.scad_file_path = file_path self.image_folder_base = Path('.') self.camera_arguments = None diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..c5ac2e76 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pytest>=7.0.0 +pathlib>=1.0.1 +dataclasses>=0.6 +typing>=3.7.4 \ No newline at end of file diff --git a/tests/test_bins.py b/tests/test_bins.py index a293649f..ad13df30 100644 --- a/tests/test_bins.py +++ b/tests/test_bins.py @@ -157,3 +157,63 @@ def test_magnet_and_screw_holes_all(self, openscad_runner): vars["chamfer_holes"] = True vars["printable_hole_top"] = True openscad_runner.create_image([], Path('magnet_and_screw_holes_all.png')) + + def test_base_locations_all_bases(self, openscad_runner): + """Test base_locations=0 (All locations)""" + vars = openscad_runner.parameters + vars["gridx"] = 3 + vars["gridy"] = 3 + vars["gridz"] = 2 + vars["base_locations"] = 0 # All locations + openscad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 350) + openscad_runner.create_image([], Path('base_locations_all_bases.png')) + + def test_base_locations_corners_only(self, openscad_runner): + """Test base_locations=1 (Corners only)""" + vars = openscad_runner.parameters + vars["gridx"] = 3 + vars["gridy"] = 3 + vars["gridz"] = 2 + vars["base_locations"] = 1 + openscad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 350) + openscad_runner.create_image([], Path('base_locations_corners_only.png')) + + def test_base_locations_edges_only(self, openscad_runner): + """Test base_locations=2 (Edges only)""" + vars = openscad_runner.parameters + vars["gridx"] = 3 + vars["gridy"] = 3 + vars["gridz"] = 2 + vars["base_locations"] = 2 + openscad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 350) + openscad_runner.create_image([], Path('base_locations_edges_only.png')) + + def test_base_locations_large_grid_all_bases(self, openscad_runner): + """Test base_locations=0 with a larger grid""" + vars = openscad_runner.parameters + vars["gridx"] = 5 + vars["gridy"] = 5 + vars["gridz"] = 2 + vars["base_locations"] = 0 + openscad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 550) + openscad_runner.create_image([], Path('base_locations_large_grid_all_bases.png')) + + def test_base_locations_large_grid_corners_only(self, openscad_runner): + """Test base_locations=1 with a larger grid""" + vars = openscad_runner.parameters + vars["gridx"] = 5 + vars["gridy"] = 5 + vars["gridz"] = 2 + vars["base_locations"] = 1 + openscad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 550) + openscad_runner.create_image([], Path('base_locations_large_grid_corners_only.png')) + + def test_base_locations_large_grid_edges_only(self, openscad_runner): + """Test base_locations=2 with a larger grid""" + vars = openscad_runner.parameters + vars["gridx"] = 5 + vars["gridy"] = 5 + vars["gridz"] = 2 + vars["base_locations"] = 2 + openscad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 550) + openscad_runner.create_image([], Path('base_locations_large_grid_edges_only.png'))