Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ For inserting magnets, check out [the jig](#jig).
- [Glued magnets](#glued-magnets)
- [Rounded corner frame](#rounded-corner-frame)
- [Solid frame](#solid-frame)
- [Click Latch](#click-latch)
- [Length](#length)
- [Distance](#distance)
- [Latch Strength](#latch-strength)
- [Latch Wall Strength](#latch-wall-strength)
- [Height](#height)
- [Steepness](#steepness)
- [Half-sized cells](#half-sized-cells)
- [Corner radius](#corner-radius)
- [Alignment](#alignment)
Expand Down Expand Up @@ -229,6 +236,80 @@ Using the `magnet_frame_style` option, you can change the magnet layer to be ful
<!-- openscad -o docs/images/magnets-solid.png --camera=0,0,0,40,0,10,200 -D plate_size='[105, 63]' -D magnets=true -D magnet_style=0 -D magnet_frame_style=0 -->
<img src="docs/images/magnets-solid.png" alt="Solid magnet frame" />

## Click Latch

If you do not wish to use magnets, but still want a more secure fit for your bins, a click latch is an option. A click latch grips the bottom of the bin.

> [!WARNING]
> When a bin is placed on the baseplate, the click latch is under constant mechanical stress. This causes the plastic to deform over time ("creep"), reducing the grip strength. _PLA is very susceptible to this._ PETG is more resistant, but long-term tests are still scarce, so _consider this feature experimental_.

<!-- openscad -o docs/images/click1.png --camera=0,0,0,40,0,10,200 -D plate_size='[105, 63]' -D click1=true -->
<img src="docs/images/click1.png" alt="Click latch" />

There are various parameters you can use to tune the click latch mechanism.

### Length

The latch is composed of two arcs at each end, and an optional middle straight section. The total length of the latch is configured using the `click1_outer_length` property:

<!-- openscad -o docs/images/click1-outer-length-20.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_outer_length=20 -->
<img src="docs/images/click1-outer-length-20.png" alt="Click latch with click1_outer_length=20" />

The length of the straight section is configured using `click1_inner_length`, which is 0 by default (no straight section). Here is an example with a 20mm straight section:

<!-- openscad -o docs/images/click1-inner-length-20.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_inner_length=20 -->
<img src="docs/images/click1-inner-length-20.png" alt="Click latch with click1_inner_length=20" />

### Distance

The `click1_distance` property changes the distance that the latch protudes into the bin area. A larger distance can increase grip strength, but makes the bin more difficult to place into the baseplate. Zero distance:

<!-- openscad -o docs/images/click1-distance-0.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_distance=0 -->
<img src="docs/images/click1-distance-0.png" alt="Click latch with click1_distance=0" />

5mm distance (don't do this):

<!-- openscad -o docs/images/click1-distance-5.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_distance=5 -->
<img src="docs/images/click1-distance-5.png" alt="Click latch with click1_distance=5" />

### Latch Strength

The `click1_strength` property controls the thickness of the latch itself. This is measured from the very bottom of the latch which, if you look at the gridfinity specification, has a chamfer of 0.7mm, so the strength needs to be higher than this to get any reasonable latch height. Here's an example with `click1_strength=2.5` (and `click1_wall_strength=0`, or else there would not be enough space):

<!-- openscad -o docs/images/click1-strength-2.5.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_strength=2.5 -D click1_wall_strength=0 -->
<img src="docs/images/click1-strength-2.5.png" alt="Click latch with click1_strength=2.5" />

### Latch Wall Strength

The `click1_wall_strength` property controls the thickness of the wall behind the latch. This wall serves two purposes: It adds rigidity to the baseplate, and it prevents the click latch from bending too far. Note that the wall is measured per cell, so if you have two neighbouring cells, the actual wall thickness will be double this value. An example with `click1_wall_strength=2` (and reduced `click1_strength`):

<!-- openscad -o docs/images/click1-wall-strength-2.png --camera=0,0,0,40,0,10,100 -D plate_size='[84, 42]' -D click1=true -D click1_strength=0.8 -D click1_wall_strength=2 -->
<img src="docs/images/click1-wall-strength-2.png" alt="Click latch with click1_wall_strength=2" />

Setting the wall strength to 0 disables the backing wall entirely:

<!-- openscad -o docs/images/click1-wall-strength-0.png --camera=0,0,0,40,0,10,100 -D plate_size='[84, 42]' -D click1=true -D click1_wall_strength=0 -->
<img src="docs/images/click1-wall-strength-0.png" alt="Click latch with click1_wall_strength=0" />

### Height

The `click1_height` property controls the height of the latch.

<!-- openscad -o docs/images/click1-height-0.5.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_height=0.5 -->
<img src="docs/images/click1-height-0.5.png" alt="Click latch with click1_height=0" />

### Steepness

The arcs of the click latch follow a logistic curve, and `click1_steepness` changes the steepness of that curve. Steepness 0.1:

<!-- openscad -o docs/images/click1-steepness-0.1.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_steepness=0.1 -->
<img src="docs/images/click1-steepness-0.1.png" alt="Click latch with click1_steepness=0.1" />

Steepness 5:

<!-- openscad -o docs/images/click1-steepness-5.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click1=true -D click1_steepness=5 -->
<img src="docs/images/click1-steepness-5.png" alt="Click latch with click1_steepness=5" />

## Half-sized cells

By default, if there isn't enough room for a full cell on the plate, GridFlock will attempt to fill up remaining space with half-sized cells.
Expand Down
Binary file added docs/images/click1-distance-0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-distance-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-height-0.5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-inner-length-20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-outer-length-20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-steepness-0.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-steepness-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-strength-2.5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-wall-strength-0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1-wall-strength-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 24 additions & 1 deletion editor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ head-extra = '<script defer src="https://um.yawk.at/script.js" data-website-id="
file = "gridflock.scad"
description-extra-html = "<br>For detailed documentation, check the <a href='https://github.com/yawkat/GridFlock/blob/main/README.md'>README</a>.<br><b>Feb 16: I've turned off magnets by default because most people don't use them.</b> If you've saved a design with magnets, you will have to manually turn them on again below."
umami-track-render = []
umami-track-export = ["magnets", "magnet_style", "connector_intersection_puzzle", "connector_edge_puzzle", "bed_size"]
umami-track-export = ["magnets", "magnet_style", "connector_intersection_puzzle", "connector_edge_puzzle", "bed_size", "click1"]

[model.param-metadata.bed_size.presets]
text = "Printer Presets"
Expand Down Expand Up @@ -128,6 +128,29 @@ help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#other-inter
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#thumb-screw"
[model.param-metadata.thumbscrew_diameter]
display-condition = {js = "thumbscrews"}
[model.param-metadata.click1]
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#click-latch"
[model.param-metadata.click1_distance]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#distance"
[model.param-metadata.click1_steepness]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#steepness"
[model.param-metadata.click1_outer_length]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#length"
[model.param-metadata.click1_inner_length]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#length"
[model.param-metadata.click1_height]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#height"
[model.param-metadata.click1_strength]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#latch_strength"
[model.param-metadata.click1_wall_strength]
display-condition = {js = "click1"}
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#latch_wall_strength"
[model.param-metadata.x_segment_algorithm]
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#horizontal"
[model.param-metadata.y_row_count_first]
Expand Down
153 changes: 148 additions & 5 deletions gridflock.scad
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ magnet_top = 0.5; // 0.25
// Floor below the magnet. Not structurally important, should be small to minimize filament use
magnet_bottom = 0.75; // 0.25

/* [Click latch (Experimental)] */

// Enable the click latch. WARNING: The plastic can deform over time, do not use PLA! PETG might be fine, but there are no long-term tests yet
click1 = false;
// Distance that the click latch extends into the bin area
click1_distance = 1; // .1
// Steepness of the click latch arc
click1_steepness = 1; // .1
// Length of the full click latch
click1_outer_length = 30;
// Length of the straight piece in the middle of the click latch. The arced pieces take up the remaining space
click1_inner_length = 0;
// Height of the click latch
click1_height = 3; // .1
// Thickness of the click latch. This is measured from the bottom of the baseplate profile
click1_strength = 1.6; // .1
// Thickness of the non-bending wall behind the click latch. This wall provides stability and prevents the click latch from bending too far
click1_wall_strength = 1; // .1

/* [Intersection Puzzle Connector] */

// Enable the intersection puzzle plate connector. This is similar to GridPlates/GRIPS. Small puzzle connectors are added cell intersections.
Expand Down Expand Up @@ -134,10 +153,13 @@ edge_adjust = [0, 0, 0, 0];
// Override the content of individual cells. Each character in this string modifies one cell. The order goes from west to east, then south to north. A 'c' stands for a normal cell. An 's' stands for a solid plate without a cell cutout. An 'e' stands for an empty square
cell_override = "";
// Test patterns
test_pattern = 0; // [0:None, 1:Half, 2:Padding, 3:Numbering, 4:Wall]
test_pattern = 0; // [0:None, 1:Half, 2:Padding, 3:Numbering, 4:Wall, 5:Click]

/* [Hidden] */

// Resolution of the click latch.
click1_steps = 15;

_MAGNET_GLUE_TOP = 0;
_MAGNET_PRESS_FIT = 1;
_MAGNET_GLUE_BOTTOM = 2;
Expand Down Expand Up @@ -245,6 +267,20 @@ module cell(half=[false, false], connector=[false, false, false, false], positiv
}
}
}
if (click1) translate([0, 0, _profile_height/2]) {
if (!half.x) cube([click1_outer_length, size.y-click1_wall_strength*2, _profile_height], center=true);
if (!half.y) cube([size.x-click1_wall_strength*2, click1_outer_length, _profile_height], center=true);
}
}
if (click1) {
if (!half.x) {
translate([0, -size.y/2, 0]) rotate([90, 0, -90]) do_sweep(_click1_sweep, convexity=4);
translate([0, size.y/2, 0]) rotate([90, 0, 90]) do_sweep(_click1_sweep, convexity=4);
}
if (!half.y) {
translate([-size.x/2, 0, 0]) rotate([90, 0, 180]) do_sweep(_click1_sweep, convexity=4);
translate([size.x/2, 0, 0]) rotate([90, 0, 0]) do_sweep(_click1_sweep, convexity=4);
}
}
if (magnets) {
translate([0, 0, -_magnet_level_height]) linear_extrude(height = _magnet_level_height) {
Expand Down Expand Up @@ -353,6 +389,96 @@ module puzzle_female(positive) {
}
}

/**
* @Summary Prepare geometry for do_sweep, which sweeps a polygon along a path
* @param polygon The polygon to sweep. Array of 2D points
* @param path The path to sweep along. Array of 3D points
* @return Geometry data to pass to do_sweep
*/
function prepare_sweep(polygon, path) = let(
ring_faces = function (base_index) [
for (i = [0:len(polygon)-1]) [
base_index + (i + 1) % len(polygon),
base_index + len(polygon) + (i + 1) % len(polygon),
base_index + len(polygon) + i,
base_index + i
]
],
points = [for (pt_path = path) each [for (pt_poly = polygon) pt_path + [pt_poly.x, pt_poly.y, 0]]],
first_face = reverse([each [0:len(polygon)-1]]),
last_face = [each [len(polygon)*(len(path)-1):len(polygon)*len(path)-1]],
faces = [
first_face,
for (i = [0:len(path)-2]) each ring_faces(i * len(polygon)),
last_face
]
) [points, faces];

/**
* @Summary Display a sweep prepared by prepare_sweep
* @param sweep The value returned by prepare_sweep
*/
module do_sweep(prep, convexity=2) {
polyhedron(points = prep[0], faces = prep[1], convexity = convexity);
}

/**
* @Summary Clip a polygon along an edge (one step of the Sutherland-Hodgman algorithm)
* @param polygon The input polygon to clip
* @param contains A lambda taking a single point that returns whether that point is clipped or not
* @param find_intersection A lambda taking a point inside and outside the result area (in that order), that returns the point where the line between those points intersects the start of the clipping region
* @return The clipped polygon, potentially with duplicate points
*/
function clip_polygon_edge(polygon, contains, find_intersection) =
[for (i = [0:len(polygon)-1]) let (
here = polygon[i],
prev = i == 0 ? polygon[len(polygon) - 1] : polygon[i - 1],
here_inside = contains(here),
prev_inside = contains(prev),
) each
here_inside ?
prev_inside ? [here] : [find_intersection(here, prev), here] :
prev_inside ? [find_intersection(prev, here)] : []
];

/**
* @Summary Clip a polygon using the Sutherland-Hodgman algorithm so that all resulting points satisfy `pt.x <= max.x && pt.y <= max.y`
* @param polygon The polygon to clip
* @param max The bounds to clip to
* @return The clipped polygon, with no duplicate points
*/
function clip_polygon_max(polygon, max) = let(
step = function(dimension, pg) clip_polygon_edge(pg, function (pt) pt[dimension] <= max[dimension], function (inside, outside) let (factor = (max[dimension] - inside[dimension]) / (outside[dimension] - inside[dimension])) inside + (outside - inside) * factor),
clipped = step(0, step(1, polygon)),
deduplicated = [for (i = 0, prev = clipped[len(clipped) - 1]; i < len(clipped); prev = clipped[i], i = i + 1) each clipped[i] == prev ? [] : [clipped[i]]]
) deduplicated;

// the maximum width of the baseplate profile (at the very bottom of the profile)
_baseplate_max_strength = _BASEPLATE_PROFILE[3].x;
// the full polygon of the baseplate profile
_baseplate_polygon = [
[0, 0],
for (pt = _BASEPLATE_PROFILE) pt + [-_baseplate_max_strength, 0]
];

_click1_polygon = let(shiftx = _baseplate_max_strength-click1_strength) [for (pt = clip_polygon_max([for (pt = _baseplate_polygon) [pt.x+shiftx, pt.y]], [0, click1_height])) [pt.x-shiftx, pt.y]];
/**
* @Summary Logistic function used for the click1 arc
* @param x coordinate
*/
function click1_path_base(x) = 1/(1+exp(-click1_steepness*x));
_click1_path = let (
arc_length = (click1_outer_length - click1_inner_length) / 2,
low = click1_path_base(-arc_length/2),
high = click1_path_base(arc_length/2),
scale = click1_distance / (high - low),
arc = [for (x = [-arc_length/2:arc_length/click1_steps:arc_length/2]) [-(click1_path_base(x)-low)*scale, 0, x - (click1_outer_length-arc_length)/2]]
) [
each arc,
for (pt = reverse(arc)) [pt.x, pt.y, -pt.z]
];
_click1_sweep = prepare_sweep(_click1_polygon, _click1_path);

/**
* @Summary Get the index of the last cell in a segment
* @param count The number of cells on each axis
Expand Down Expand Up @@ -659,6 +785,11 @@ module chamfer_triangle() {
polygon([[-extend, -extend], [1 + extend, -extend], [-extend, 1 + extend]]);
}

function compute_segment_size(count, padding) = [
BASEPLATE_DIMENSIONS.x * count.x + padding[_EAST] + padding[_WEST],
BASEPLATE_DIMENSIONS.y * count.y + padding[_NORTH] + padding[_SOUTH],
];

/**
* @Summary Model a segment, which is piece of the plate without breaks
* @param count The number of cells in this segment, on each axis
Expand All @@ -669,10 +800,7 @@ module chamfer_triangle() {
* @param global_cell_count If applicable, the global cell count. This is used for vertical screws at plate corners
*/
module segment(count=[1, 1], padding=[0, 0, 0, 0], connector=[false, false, false, false], global_segment_index=undef, global_cell_index=[0, 0], global_cell_count=[0, 0]) {
size = [
BASEPLATE_DIMENSIONS.x * count.x + padding[_EAST] + padding[_WEST],
BASEPLATE_DIMENSIONS.y * count.y + padding[_NORTH] + padding[_SOUTH],
];
size = compute_segment_size(count, padding);
_edge_puzzle_height_male = edge_puzzle_height_female - edge_puzzle_height_male_delta;
// whether to cut the male edge puzzle connector to make room for the bin in the next cell. For really short connectors this is not necessary, but there's also no good reason to turn this off, so it's not user configurable at the moment
_edge_puzzle_overlap = true;
Expand Down Expand Up @@ -1014,6 +1142,19 @@ module test_pattern_wall() {
segment(count = [2, 2], connector=[false, false, false, false], padding=[5, 5, 5, 5]);
}

module test_pattern_click() {
count = [1, 3];
// this should give similar wall strength as a neighbouring cell
padding = [12, click1_wall_strength, click1_wall_strength, click1_wall_strength];
segment(count = count, connector=[false, false, false, false], padding=padding);
format_small = function (d) d < 1 ? str(".", d*10) : (d % 1) == 0 ? str(d) : str(d * 10);
txt = click1 ? str(format_small(click1_distance), "|", format_small(click1_steepness), "|", format_small(click1_height), "|", format_small(click1_strength), "|", format_small(click1_wall_strength)) : "off";
navigate_edge(size = compute_segment_size(count, padding), count = count, padding = padding, index = [0, 2], dir = _NORTH)
translate([0, padding[_NORTH]/2, _profile_height])
linear_extrude(0.5)
scale([0.7, 1]) text(txt, halign="center", valign="center", size=8);
}

if (test_pattern == 0) {
main();
} else if (test_pattern == 1) {
Expand All @@ -1024,4 +1165,6 @@ if (test_pattern == 0) {
test_pattern_numbering();
} else if (test_pattern == 4) {
test_pattern_wall();
} else if (test_pattern == 5) {
test_pattern_click();
}
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ docs:
written.append(output)
for f in os.listdir("docs/images"):
if os.path.join("docs/images", f) not in written:
os.unlink(f)
os.unlink(os.path.join("docs/images", f))
await asyncio.gather(*tasks)

asyncio.run(main())
Expand Down
4 changes: 4 additions & 0 deletions test.scad
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ assert_eq([3, 6, 1], plan_axis_incremental_vars(axis_norm=10, bed_norm=6.1, star
assert_eq([2, 6, 2], plan_axis_incremental_vars(axis_norm=10, bed_norm=6.1, start_padding_norm=0.2, end_padding_norm=0.2, force_first=2));
assert_eq([1, 6, 3], plan_axis_incremental_vars(axis_norm=10, bed_norm=6.1, start_padding_norm=0.2, end_padding_norm=0.2, force_first=1));

assert_eq([[0, 1], [0, 0], [1, 0], [1, 1]], clip_polygon_max([[0, 0], [2, 0], [0, 2]], [1, 1]));
assert_eq([[1, 1], [0, 1], [0, 0], [1, 0]], clip_polygon_max([[0, 2], [0, 0], [2, 0]], [1, 1]));
assert_eq([[1, 0], [1, 1], [0, 1], [0, 0]], clip_polygon_max([[2, 0], [0, 2], [0, 0]], [1, 1]));

cube([1, 1, 1]);