diff --git a/README.md b/README.md index 950b19a..2ae7cf4 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ For inserting magnets, check out [the jig](#jig). - [None](#none) - [Integer Fraction](#integer-fraction) - [Dynamic](#dynamic) + - [Lightweight](#lightweight) + - [Wall thickness](#wall-thickness) + - [Bottom lip](#bottom-lip) - [Corner radius](#corner-radius) - [Alignment](#alignment) - [Custom cell size](#custom-cell-size) @@ -462,6 +465,39 @@ If you wish to only create larger cells, not smaller cells, you can set the mini Dynamic filler always expanded +## Lightweight + +`hollow` skeletonizes the baseplate: instead of solid material between the cells, only a thin wall following the Gridfinity profile is printed. Neighbouring cells share a hollow channel, open towards the bed and closed at the top by a self-supporting 45° roof. Without magnets this roughly halves filament use and print time — a 4x4 plate drops from 20.6cm³ to 10.5cm³. + + +Lightweight baseplate, seen from below + +The same plate without `hollow`, also seen from below: + + +Standard baseplate, seen from below + +The top surface, the outer wall of each segment and the puzzle connectors stay solid, so segments link up the same way and the plate looks unchanged from above. + +> [!NOTE] +> Hollow mode is incompatible with magnets, `solid_base` and the click latch — there is no material left to hold them, and GridFlock refuses to render that combination. + +### Wall thickness + +`hollow_wall` sets the wall thickness. It is a *horizontal* thickness, which is what the slicer sees on each layer, so setting it to your nozzle diameter prints the plate as a single wall with no infill. The default 0.8mm is one wall on a 0.8mm nozzle, two to three on a 0.4mm nozzle. + + +Lightweight baseplate with thicker walls + +### Bottom lip + +The Gridfinity profile ends in a 0.7mm lip that slopes back inwards. It is not functionally required; `remove_bottom_lip` replaces it with a straight continuation of the vertical section. This saves a little filament, removes the overhang where two cells meet, and gives the first layer a flat contact area instead of a knife edge. + + +Baseplate with the bottom lip removed + +It can be used on its own, and is always enabled in hollow mode, where the inward slope would taper the horizontally measured walls to nothing at the first layer. + ## Corner radius GridFlock generates plates with a corner radius of 4mm by default. This matches the Gridfinity corner radius, so when the plate size is a perfect multiple of 42, there is no gap. diff --git a/docs/images/bottom-lip.png b/docs/images/bottom-lip.png new file mode 100644 index 0000000..b9b3a29 Binary files /dev/null and b/docs/images/bottom-lip.png differ diff --git a/docs/images/lightweight-off.png b/docs/images/lightweight-off.png new file mode 100644 index 0000000..90fe95f Binary files /dev/null and b/docs/images/lightweight-off.png differ diff --git a/docs/images/lightweight-wall.png b/docs/images/lightweight-wall.png new file mode 100644 index 0000000..63aee80 Binary files /dev/null and b/docs/images/lightweight-wall.png differ diff --git a/docs/images/lightweight.png b/docs/images/lightweight.png new file mode 100644 index 0000000..e4644a4 Binary files /dev/null and b/docs/images/lightweight.png differ diff --git a/editor.toml b/editor.toml index 781855b..8999603 100644 --- a/editor.toml +++ b/editor.toml @@ -14,10 +14,11 @@ description-extra-html = """
Apr 8: Glue-in magnets now have a release slot, which can be disabled by setting magnet_release_width to 0. + """ umami-track-render = [] -umami-track-export = ["magnets", "magnet_style", "connector_intersection_puzzle", "connector_edge_puzzle", "bed_size", "click", "click_style", "filler_x", "filler_y", "stacked_print", "adapter_mode"] +umami-track-export = ["magnets", "magnet_style", "connector_intersection_puzzle", "connector_edge_puzzle", "bed_size", "click", "click_style", "filler_x", "filler_y", "stacked_print", "adapter_mode", "hollow"] [model.param-metadata.bed_size.presets] text = "Printer Presets" @@ -129,6 +130,18 @@ display-condition = {js = "filler_x == 1 || filler_y == 1"} help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#dynamic" display-condition = {js = "filler_x == 2 || filler_y == 2"} +[model.tab-metadata."Lightweight"] +control-boolean = "hollow" +description-html = "Skeletonize the baseplate so only thin walls are printed. This roughly halves filament use and print time, but leaves no material for magnets, a solid base or the click latch." +help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#lightweight" +[model.param-metadata.hollow] +display-condition = {fixed = false} +[model.param-metadata.hollow_wall] +help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#wall-thickness" +[model.param-metadata.remove_bottom_lip] +help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#bottom-lip" +display-condition = {js = "!hollow"} + [model.tab-metadata."Numbering"] help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#numbering" collapsed = true diff --git a/gridflock.scad b/gridflock.scad index 44ceaae..5ab6841 100644 --- a/gridflock.scad +++ b/gridflock.scad @@ -103,6 +103,15 @@ filler_minimum_size = [15, 15]; // Padding alignment. The first value is the x direction (east/west), the second value the y direction (north/south). When padding is added to the build plate, this alignment is used to distribute it. A lower value will move the grid towards the west/south direction, adding more padding to the east/north alignment = [0.5, 0.5]; // [0:0.1:1] +/* [Lightweight] */ + +// Skeletonize the baseplate: instead of solid material between the cells, only a thin wall following the gridfinity profile is printed. This roughly halves filament use and print time. Incompatible with magnets, a solid base and the click latch +hollow = false; +// Wall thickness in hollow mode. This is a horizontal thickness, which is what the slicer sees on each layer, so a value matching your nozzle diameter prints as a single wall +hollow_wall = 0.8; // 0.05 +// Remove the bottom lip of the gridfinity profile, extending the vertical section of the profile straight down instead. The lip is not functionally required; removing it saves filament, removes the overhang between neighbouring cells, and widens the first layer. Always enabled in hollow mode +remove_bottom_lip = false; + /* [Numbering] */ // Enable numbering of the segements, embossed in a corner @@ -256,6 +265,10 @@ assert(!magnets || magnet_frame_style != _MAGNET_SOLID || magnet_style != _MAGNE assert(!thumbscrews || solid_base > 0 || (magnets && magnet_frame_style == _MAGNET_SOLID), "Thumbscrew holes require some sort of solid base, such as magnet_style solid, or an explicit solid_base."); +assert(!hollow || (!magnets && solid_base == 0 && !click), "Hollow mode only leaves a thin wall around the gridfinity profile, so there is nothing left to hold magnets, a solid base or a click latch."); + +assert(hollow_wall > 0, "hollow_wall must be positive."); + _OPENGRID_LITE = 1; _OPENGRID_DIRECTIONAL = 2; _OPENGRID_VERTICAL = 4; @@ -276,6 +289,12 @@ _magnet_extraction_dim_negative = [magnet_release_width, magnet_diameter/2]; _profile_height_raw = 4.65; // for stacked prints, we cut off a sliver at the top to get a better contact area _profile_height = _profile_height_raw - top_slice - (stacked_print ? stacked_print_slice : 0); +// The lip slopes inward, so in hollow mode it would taper the horizontally measured shell to nothing +_remove_bottom_lip = remove_bottom_lip || hollow; +// Horizontal distance from the sweep path to the vertical (waist) section of the gridfinity profile +_profile_waist_offset = BASEPLATE_INNER_RADIUS + _BASEPLATE_PROFILE[1].x; +// Height of the bottom lip of the gridfinity profile +_profile_lip_height = _BASEPLATE_PROFILE[1].y; // height of the magnet level _magnet_level_height = (magnet_style != _MAGNET_GLUE_TOP ? magnet_top : 0) + (magnet_style != _MAGNET_GLUE_BOTTOM ? magnet_bottom : 0) + magnet_height; // total height of the non-bin levels (magnets, solid base). These are placed at z<0 @@ -373,6 +392,24 @@ module cutter(size, below=0) { } } } + // Extend the waist straight down, removing the lip and the overhang between neighbouring cells + if (_remove_bottom_lip) { + floor = -below - 0.001; + translate([0, 0, floor]) linear_extrude(_profile_lip_height - floor) offset(_profile_waist_offset) + square([size.x - BASEPLATE_OUTER_DIAMETER, size.y - BASEPLATE_OUTER_DIAMETER], center=true); + } +} + +/** + * @Summary The interior volume of a cell that hollow mode removes, leaving a hollow_wall shell + * @param unit_size Size of the cell, in grid units, in each direction + */ +module cell_core(unit_size=[1, 1]) { + size = [BASEPLATE_DIMENSIONS.x*unit_size.x, BASEPLATE_DIMENSIONS.y*unit_size.y]; + difference() { + translate([-size.x/2, -size.y/2, -0.001]) cube([size.x, size.y, _total_height + 0.002]); + cutter([size.x + hollow_wall*2, size.y + hollow_wall*2]); + } } /** @@ -1024,6 +1061,33 @@ function compute_segment_size(trace, padding) = [ BASEPLATE_DIMENSIONS.y * cumulate(trace.y)[len(trace.y)] + padding[_NORTH] + padding[_SOUTH], ]; +/** + * @Summary Decide how a single cell is rendered, from cell_override + * @param index Index of the cell within the segment + */ +function cell_style(index, global_cell_index, global_cell_count) = let( + seq_index = (index.y + global_cell_index.y) * ceil(global_cell_count.x) + (index.x + global_cell_index.x) +) len(cell_override) > seq_index ? cell_override[seq_index] : _CELL_STYLE_NORMAL; + +/** + * @Summary The union of the cell cores, clipped so the outer wall and its puzzle connectors stay solid + */ +module segment_core(trace, size, padding, connector, global_cell_index, global_cell_count) { + last = [len(trace.x)-1, len(trace.y)-1]; + intersection() { + translate([0, 0, -_extra_height]) linear_extrude(height = _total_height) + offset(-hollow_wall) segment_rectangle(size, connector, include_wall=false); + union() { + for (ix = [0:1:last.x]) for (iy = [0:1:last.y]) navigate_cell(size, trace, padding, [ix, iy]) { + cell_size = [trace.x[ix], trace.y[iy]]; + if (cell_style([ix, iy], global_cell_index, global_cell_count) == _CELL_STYLE_NORMAL) { + cell_core(cell_size); + } + } + } + } +} + /** * @Summary Model a segment, which is piece of the plate without breaks * @param trace The cell sizes, in grid units, on each axis @@ -1061,21 +1125,20 @@ module segment(trace=[[1], [1]], padding=[0, 0, 0, 0], connector=[false, false, for (ix = [0:1:last.x]) for (iy = [0:1:last.y]) navigate_cell(size, trace, padding, [ix, iy]) { cell_size = [trace.x[ix], trace.y[iy]]; - seq_index = (iy + global_cell_index.y) * ceil(global_cell_count.x) + (ix + global_cell_index.x); - cell_style = len(cell_override) <= seq_index ? _CELL_STYLE_NORMAL : cell_override[seq_index]; - if (cell_style == _CELL_STYLE_NORMAL) { + style = cell_style([ix, iy], global_cell_index, global_cell_count); + if (style == _CELL_STYLE_NORMAL) { at_edge = [iy == last.y, ix == last.x, iy == 0, ix == 0]; cell( cell_size, connector=[for (direction = [0:3]) connector[direction] && at_edge[direction]], bottom_chamfer_takes=[for (direction = [0:3]) at_edge[direction] && !connector[direction] ? max(0, bottom_chamfer[direction] - padding[direction]) : 0] ); - } else if (cell_style == _CELL_STYLE_EMPTY) { - } else if (cell_style == _CELL_STYLE_SOLID) { + } else if (style == _CELL_STYLE_EMPTY) { + } else if (style == _CELL_STYLE_SOLID) { dim = [BASEPLATE_DIMENSIONS.x * cell_size.x, BASEPLATE_DIMENSIONS.y * cell_size.y]; translate([-dim.x/2, -dim.y/2, -_extra_height]) cube([dim.x, dim.y, _total_height]); } else { - assert(false, str("Unknown cell style: '", cell_style, "'")); + assert(false, str("Unknown cell style: '", style, "'")); } }; }; @@ -1222,6 +1285,8 @@ module segment(trace=[[1], [1]], padding=[0, 0, 0, 0], connector=[false, false, translate([0, -size.y/2]) rotate([0, 0, 180]) horizontal_screws(_SOUTH, padding, trace = trace.x, connector = connector); translate([-size.x/2, 0]) rotate([0, 0, 90]) horizontal_screws(_WEST, padding, trace = trace.y, connector = connector); translate([size.x/2, 0]) rotate([0, 0, -90]) horizontal_screws(_EAST, padding, trace = trace.y, connector = connector); + + if (hollow) segment_core(trace, size, padding, connector, global_cell_index, global_cell_count); } }