Skip to content
Open
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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This is an openscad-based generator for Gridfinity baseplates.

* Any commits assisted by AI must be tagged with appropriate `Co-Authored-By`.
* Any new options must be documented in `README.md` and added to `editor.toml`.
* Any new chapters in `README.md` must have an entry in the table of contents.
* `editor.toml` is used by the web interface and is documented in https://github.com/yawkat/web-openscad-editor/blob/main/README.md
* `README.md` includes example images. Next to each image is a HTML comment that contains the openscad command used to generate it.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ For inserting magnets, check out [the jig](#jig).
- [Segmentation](#segmentation)
- [Horizontal](#horizontal)
- [Vertical](#vertical)
- [Separate edge padding](#separate-edge-padding)
- [Edge Adjustment](#edge-adjustment)
- [Shifting the grid](#shifting-the-grid)
- [Adding empty space](#adding-empty-space)
Expand Down Expand Up @@ -692,6 +693,13 @@ You can override the cell count for the first segment of each plan using the `y_
<!-- openscad -o docs/images/segment-y-override.png --camera=0,0,0,40,0,10,600 -D plate_size='[84, 252]' -D bed_size='[50, 180]' -D magnets=false -D connector_intersection_puzzle=false -D y_row_count_first='[1, 1]' -->
<img src="docs/images/segment-y-override.png" alt="Incremental segmentation alogirthm with cell count override (y splitting)" />

### Separate edge padding

By default, positive edge padding is attached to the outer baseplate segments. Enable `separate_edge_padding` to generate positive edge padding as separate printable strips instead. The strips are planned as normal segments, so they include the connectors needed to attach them to the neighboring baseplate segments.

<!-- openscad -o docs/images/separate-edge.png --camera=0,0,0,40,0,10,600 -D plate_size='[180, 180]' -D bed_size='[100, 100]' -D connector_intersection_puzzle=true -D separate_edge_padding=true -->
<img src="docs/images/separate-edge.png" alt="Separate edge padding" />

## Edge Adjustment

The basic GridFlock cell placement algorithm is simple: Fit as many cells into the configured `plate_size`, and evenly distribute any remaining space as padding along the edges.
Expand Down
Binary file modified docs/images/bottom-chamfer.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 modified 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 modified docs/images/click2-bin.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 modified docs/images/closeup.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 modified docs/images/edge-puzzle-full-height.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 modified docs/images/edge-puzzle-multi.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 modified docs/images/edge-puzzle-unconnected.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 modified docs/images/edge-puzzle.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 modified docs/images/filler-dynamic-expand.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 modified docs/images/filler-dynamic.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 modified docs/images/intersection-puzzle.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 modified docs/images/jig-main.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 modified docs/images/numbering-squeeze.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 modified docs/images/segment-y-override.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 modified docs/images/segment-y.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/separate-edge.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 modified docs/images/vscrews-plate-edges.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 modified docs/images/vscrews-segment-edges.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 modified docs/images/wall-bottom.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 modified docs/images/wall-top.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 modified docs/images/whole.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions editor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#horizontal"
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#horizontal"
[model.param-metadata.x_column_count_first]
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#vertical"
[model.param-metadata.separate_edge_padding]
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#separate-edge-padding"

[model.tab-metadata."Stacked Print"]
help-link = "https://github.com/yawkat/GridFlock/blob/main/README.md#stacked-print"
Expand Down
92 changes: 60 additions & 32 deletions gridflock.scad
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ x_segment_algorithm = 0; // [0:Ideal, 1:Incremental]
y_row_count_first = [0, 0];
// If the 'incremental' x segment algorithm is chosen, this can be used to override the column count in the first segment.
x_column_count_first = 0;
// Generate positive edge padding as separate printable pieces instead of attaching it to the outer baseplate segments.
separate_edge_padding = false;

/* [Stacked Print] */

Expand Down Expand Up @@ -660,6 +662,12 @@ module navigate_edge(size, trace, padding, index, dir) {
*/
module segment_intersection_connectors(positive, trace, size, padding, connector) {
last = [len(trace.x) - 1, len(trace.y) - 1];
connector = [
connector[_NORTH] && len(trace.x) > 0,
connector[_EAST] && len(trace.y) > 0,
connector[_SOUTH] && len(trace.x) > 0,
connector[_WEST] && len(trace.y) > 0
];
// for the normal case, we iterate over the cells at the edge of the segment, and add two half-connectors for each cell.
for (ix = [0:1:last.x]) {
// north and south connectors
Expand Down Expand Up @@ -842,6 +850,16 @@ module segment_corner(posy=_NORTH, posx=_WEST, connector=[false, false, false, f
}
}

/**
* This is an "inverted" quarter-circle that is used to punch out the corner of a rounded rectangle.
*/
module corner_punch() {
difference() {
square([plate_corner_radius, plate_corner_radius]);
translate([plate_corner_radius, plate_corner_radius]) circle(r=plate_corner_radius);
}
}

/**
* @Summary Draw the 2D shape of a segment, including rounded corners
* @param size The size of the segment
Expand All @@ -853,21 +871,13 @@ module segment_rectangle(size, connector=[false, false, false, false], include_w
wall_t = function (side) include_wall || connector[side] ? 0 : plate_wall_thickness[side];
// corner radius by side
compute_radius = function (side) max(0.01, plate_corner_radius - wall_t(side));
bounds_offset = function (side) compute_radius(side) + wall_t(side);
bounds_min = [
-size.x/2 + bounds_offset(_WEST),
-size.y/2 + bounds_offset(_SOUTH)
];
bounds_max = [
size.x/2 - bounds_offset(_EAST),
size.y/2 - bounds_offset(_NORTH)
];
hull() {
translate([bounds_min.x, bounds_min.y]) segment_corner(_SOUTH, _WEST, connector, compute_radius);
translate([bounds_max.x, bounds_min.y]) segment_corner(_SOUTH, _EAST, connector, compute_radius);
translate([bounds_max.x, bounds_max.y]) segment_corner(_NORTH, _EAST, connector, compute_radius);
translate([bounds_min.x, bounds_max.y]) segment_corner(_NORTH, _WEST, connector, compute_radius);
};
difference() {
square(size, center=true);
if (!connector[_SOUTH] && !connector[_WEST]) translate([-size.x/2, -size.y/2]) corner_punch();
if (!connector[_NORTH] && !connector[_WEST]) translate([-size.x/2, size.y/2]) rotate(-90) corner_punch();
if (!connector[_SOUTH] && !connector[_EAST]) translate([size.x/2, -size.y/2]) rotate(90) corner_punch();
if (!connector[_NORTH] && !connector[_EAST]) translate([size.x/2, size.y/2]) rotate(180) corner_punch();
}
}

module chamfer_triangle() {
Expand Down Expand Up @@ -964,8 +974,13 @@ module segment(trace=[[1], [1]], padding=[0, 0, 0, 0], connector=[false, false,
translate([0, 0, -_extra_height]) linear_extrude(height = _extra_height+edge_puzzle_height_female) segment_edge_connectors(false, trace, size, padding, connector);
}
if (numbering && global_segment_index != undef) {
grid_segment = len(trace.x) > 0 && len(trace.y) > 0;
squeeze = len(trace.x) <= 1;
navigate_cell(size, trace, padding, [0, 0]) translate([BASEPLATE_DIMENSIONS.x*trace.x[0]/2-(squeeze?2.95/2:0), -BASEPLATE_DIMENSIONS.y/2+4, -_extra_height]) linear_extrude(number_depth) mirror([0, 1]) rotate([0, 0, 90]) text(str(global_segment_index + 1), size = squeeze ? number_squeeze_size : number_size, halign="right", valign = "center", font = number_font);
if (grid_segment) {
navigate_cell(size, trace, padding, [0, 0]) translate([BASEPLATE_DIMENSIONS.x*trace.x[0]/2-(squeeze?2.95/2:0), -BASEPLATE_DIMENSIONS.y/2+4, -_extra_height]) linear_extrude(number_depth) mirror([0, 1]) rotate([0, 0, 90]) text(str(global_segment_index + 1), size = squeeze ? number_squeeze_size : number_size, halign="right", valign = "center", font = number_font);
} else {
translate([0, 0, -_extra_height]) linear_extrude(number_depth) mirror([0, 1]) text(str(global_segment_index + 1), size = number_squeeze_size, halign="center", valign = "center", font = number_font);
}
}
// extend a bit beyond the segment edges to make sure we cut any overhang
extend = 10;
Expand Down Expand Up @@ -999,6 +1014,9 @@ module segment(trace=[[1], [1]], padding=[0, 0, 0, 0], connector=[false, false,
}
}

function padding_unless_separate(p) =
separate_edge_padding ? min(0, p) : p;

/**
* @Summary Calculate the minimum number of segments required to print this axis
* @param trace The cell sizes on this axis
Expand All @@ -1025,15 +1043,16 @@ function segments_per_axis(trace, bed_norm, start_padding_norm=0, end_padding_no
function plan_axis_ideal(trace, bed_norm, start_padding_norm=0, end_padding_norm=0) =
let(
cumulated = cumulate(trace),
total_size = cumulated[len(trace)] + start_padding_norm + end_padding_norm,
segment_count = segments_per_axis(trace, bed_norm, start_padding_norm, end_padding_norm),
total_size = cumulated[len(trace)] + padding_unless_separate(start_padding_norm) + padding_unless_separate(end_padding_norm),
segment_count = segments_per_axis(trace, bed_norm, padding_unless_separate(start_padding_norm), padding_unless_separate(end_padding_norm)),
avg_segment_size = total_size / segment_count,
segment_count_with_padding = segment_count + (separate_edge_padding && start_padding_norm > 0 ? 1 : 0) + (separate_edge_padding && end_padding_norm > 0 ? 1 : 0),
// compute which segment each cell is assigned to
assignments = [for (i = [0:len(trace) - 1]) let (
center = cumulated[i] + trace[i] / 2 + start_padding_norm,
center = cumulated[i] + trace[i] / 2 + padding_unless_separate(start_padding_norm),
norm_ix = center / avg_segment_size
) (norm_ix % 1) == 0 ? norm_ix - 1 : floor(norm_ix)]
) [for (i = [0:segment_count - 1]) len(search(i, assignments, num_returns_per_match=0))];
) ((norm_ix % 1) == 0 ? norm_ix - 1 : floor(norm_ix)) + (separate_edge_padding && start_padding_norm > 0 ? 1 : 0)]
) [for (i = [0:segment_count_with_padding - 1]) len(search(i, assignments, num_returns_per_match=0))];

/**
* @Summary Calculate an incremental axis plan.
Expand Down Expand Up @@ -1076,13 +1095,15 @@ function plan_axis_incremental_vars(trace, bed_norm, start_padding_norm=0, end_p
* @Summary Transform a short plan from plan_axis_incremental_vars into a full plan as returned by plan_axis_ideal
* @return A vector containing the number of cells in each planned segment
*/
function vars_to_incremental(trace, vars) = let(
function vars_to_incremental(trace, vars, start_padding_norm=0, end_padding_norm=0) = let(
axis_norm = len(trace),
first = vars[0],
mid = vars[1],
end = vars[2]
) mid == -1 ? [first] : [for(i = 0, pos = 0; pos < axis_norm; i = i + 1, pos = first + mid * (i - 1))
i == 0 ? first : pos + mid >= axis_norm ? end : mid];
end = vars[2],
before = separate_edge_padding && start_padding_norm > 0 ? [0] : [],
after = separate_edge_padding && end_padding_norm > 0 ? [0] : []
) concat(before, (mid == -1 ? [first] : [for(i = 0, pos = 0; pos < axis_norm; i = i + 1, pos = first + mid * (i - 1))
i == 0 ? first : pos + mid >= axis_norm ? end : mid]), after);

/**
* @Summary Score plan_b, assuming plan_a is fixed. Lower value is better
Expand Down Expand Up @@ -1120,7 +1141,7 @@ function plan_axis_staggered(trace, bed_norm, start_padding_norm=0, end_padding_
assert(end_padding_norm != undef)
let (
// lambda: call plan_axis_incremental_vars with a specific shift
plan_vars = function(force_first) plan_axis_incremental_vars(trace, bed_norm, start_padding_norm, end_padding_norm, force_first),
plan_vars = function(force_first) plan_axis_incremental_vars(trace, bed_norm, padding_unless_separate(start_padding_norm), padding_unless_separate(end_padding_norm), force_first),
// lambda: calculate the number of segments for a given set of plan_axis_incremental_vars
plan_size = function(vars) vars[1] == -1 ? 1 : (len(trace) - vars[0] - vars[2]) / vars[1] + 2,
// make a simple plan for the first column
Expand All @@ -1129,9 +1150,9 @@ function plan_axis_staggered(trace, bed_norm, start_padding_norm=0, end_padding_
plan_a2 = plan_a1[1] == -1 || plan_a1[2] >= 2 || plan_a1[0] <= 2 ? plan_a1 : plan_vars(plan_a1[0] - 1)
)
// manual override
y_row_count_first[1] > 0 ? [vars_to_incremental(trace, plan_a1), vars_to_incremental(trace, plan_vars(y_row_count_first[1]))] :
y_row_count_first[1] > 0 ? [vars_to_incremental(trace, plan_a1, start_padding_norm, end_padding_norm), vars_to_incremental(trace, plan_vars(y_row_count_first[1]), start_padding_norm, end_padding_norm)] :
// shortcut: if we don't need to split at all, or we can't change the split, we don't need to worry about staggering
plan_a1[1] <= 1 ? [vars_to_incremental(trace, plan_a1), vars_to_incremental(trace, plan_a1)] :
plan_a1[1] <= 1 ? [vars_to_incremental(trace, plan_a1, start_padding_norm, end_padding_norm), vars_to_incremental(trace, plan_a1, start_padding_norm, end_padding_norm)] :
let(
// now, we determine the optimal shift of the second column.
// first, plan with a minimum shift as a baseline.
Expand All @@ -1148,7 +1169,7 @@ function plan_axis_staggered(trace, bed_norm, start_padding_norm=0, end_padding_
) score_plan_b(plan_a2, plan)],
// pick the shift with the best score
shift = least_index(plan_b_shift) + 1
) [vars_to_incremental(trace, plan_a2), vars_to_incremental(trace, plan_vars(plan_a2[0] - shift))];
) [vars_to_incremental(trace, plan_a2, start_padding_norm, end_padding_norm), vars_to_incremental(trace, plan_vars(plan_a2[0] - shift), start_padding_norm, end_padding_norm)];

/**
* @Summary Quicksort the input array
Expand Down Expand Up @@ -1230,14 +1251,21 @@ module main() {
// for the x axis, we only need a single plan, so we can use the ideal algorithm.
plan_x = x_segment_algorithm == _SEGMENT_ALGORITHM_IDEAL ?
plan_axis_ideal(global_trace.x, bed_norm=bed_norm.x, start_padding_norm=start_padding_norm.x, end_padding_norm=end_padding_norm.x) :
vars_to_incremental(global_trace.x, plan_axis_incremental_vars(global_trace.x, bed_norm=bed_norm.x, start_padding_norm=start_padding_norm.x, end_padding_norm=end_padding_norm.x, force_first=x_column_count_first == 0 ? undef : x_column_count_first));
vars_to_incremental(global_trace.x, plan_axis_incremental_vars(global_trace.x, bed_norm=bed_norm.x, start_padding_norm=start_padding_norm.x, end_padding_norm=end_padding_norm.x, force_first=x_column_count_first == 0 ? undef : x_column_count_first), start_padding_norm.x, end_padding_norm.x);
// for the y axis, we need to avoid 4-way gap intersections, so we need two plans.
plans_y = plan_axis_staggered(global_trace.y, bed_norm=bed_norm.y, start_padding_norm=start_padding_norm.y, end_padding_norm=end_padding_norm.y);
plans_y_cumulate = [for (p = plans_y) cumulate(p)];
plan_x_cumulate = cumulate(plan_x);

function get_plan_y(segix) = plans_y[segix % 2];
function get_plan_y_cumulate(segix) = plans_y_cumulate[segix % 2];
// with separate_edge_padding, the normal plans contain segments with 0x0 cells that we want to avoid. those segments are collapsed into their neighbors here.
function collapse_empty(plan) =
[for (seg = plan) if (seg != 0) seg];
plans_y_collapsed = [for (p = plans_y) collapse_empty(p)];
plans_y_collapsed_cumulate = [for (p = plans_y_collapsed) cumulate(p)];

function get_plan_y(segix) =
(plan_x[segix] == 0 ? plans_y_collapsed : plans_y)[segix % 2];
function get_plan_y_cumulate(segix) = (plan_x[segix] == 0 ? plans_y_collapsed_cumulate : plans_y_cumulate)[segix % 2];

/*
* @Summary Compute the padding for a particular segment.
Expand Down
Loading