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
+## 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³.
+
+
+
+
+The same plate without `hollow`, also 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.
+
+
+
+
+### 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.
+
+
+
+
+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.
The click latch can deform over time, especially if made of PLA. Prefer other materials like PETG.
The click latch can deform over time if used with non-ClickGroove bins, especially if made of PLA. Prefer other materials like PETG if you plan to use such bins.
+Hollow mode only leaves a thin wall around the gridfinity profile, so it cannot be combined with magnets, a solid base or the click latch.
You have enabled both the edge and intersection puzzle connectors. While technically possible, it is better to focus on and tune one connector type.
"""
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);
}
}