diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f36af1f
--- /dev/null
+++ b/AGENTS.md
@@ -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.
diff --git a/README.md b/README.md
index 304fd51..d02013b 100644
--- a/README.md
+++ b/README.md
@@ -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)
@@ -692,6 +693,13 @@ You can override the cell count for the first segment of each plan using the `y_
+### 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.
+
+
+
+
## 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.
diff --git a/docs/images/bottom-chamfer.png b/docs/images/bottom-chamfer.png
index 7201dba..26aa70b 100644
Binary files a/docs/images/bottom-chamfer.png and b/docs/images/bottom-chamfer.png differ
diff --git a/docs/images/click1-strength-2.5.png b/docs/images/click1-strength-2.5.png
index bf58dae..03bfd2c 100644
Binary files a/docs/images/click1-strength-2.5.png and b/docs/images/click1-strength-2.5.png differ
diff --git a/docs/images/click2-bin.png b/docs/images/click2-bin.png
index 520d859..4f33a22 100644
Binary files a/docs/images/click2-bin.png and b/docs/images/click2-bin.png differ
diff --git a/docs/images/closeup.png b/docs/images/closeup.png
index 3e09bdd..011f4f2 100644
Binary files a/docs/images/closeup.png and b/docs/images/closeup.png differ
diff --git a/docs/images/edge-puzzle-full-height.png b/docs/images/edge-puzzle-full-height.png
index 110f78a..338fd37 100644
Binary files a/docs/images/edge-puzzle-full-height.png and b/docs/images/edge-puzzle-full-height.png differ
diff --git a/docs/images/edge-puzzle-multi.png b/docs/images/edge-puzzle-multi.png
index b1becde..43133b9 100644
Binary files a/docs/images/edge-puzzle-multi.png and b/docs/images/edge-puzzle-multi.png differ
diff --git a/docs/images/edge-puzzle-unconnected.png b/docs/images/edge-puzzle-unconnected.png
index 99895cd..d1cefbd 100644
Binary files a/docs/images/edge-puzzle-unconnected.png and b/docs/images/edge-puzzle-unconnected.png differ
diff --git a/docs/images/edge-puzzle.png b/docs/images/edge-puzzle.png
index 4495a90..95ef8a4 100644
Binary files a/docs/images/edge-puzzle.png and b/docs/images/edge-puzzle.png differ
diff --git a/docs/images/filler-dynamic-expand.png b/docs/images/filler-dynamic-expand.png
index 3f64492..125e9f8 100644
Binary files a/docs/images/filler-dynamic-expand.png and b/docs/images/filler-dynamic-expand.png differ
diff --git a/docs/images/filler-dynamic.png b/docs/images/filler-dynamic.png
index ced6cde..6659b04 100644
Binary files a/docs/images/filler-dynamic.png and b/docs/images/filler-dynamic.png differ
diff --git a/docs/images/intersection-puzzle.png b/docs/images/intersection-puzzle.png
index 3e09bdd..011f4f2 100644
Binary files a/docs/images/intersection-puzzle.png and b/docs/images/intersection-puzzle.png differ
diff --git a/docs/images/jig-main.png b/docs/images/jig-main.png
index bc19f79..8fdac12 100644
Binary files a/docs/images/jig-main.png and b/docs/images/jig-main.png differ
diff --git a/docs/images/numbering-squeeze.png b/docs/images/numbering-squeeze.png
index ab4abdd..8a3b1b6 100644
Binary files a/docs/images/numbering-squeeze.png and b/docs/images/numbering-squeeze.png differ
diff --git a/docs/images/segment-y-override.png b/docs/images/segment-y-override.png
index 6f854af..3bcce5a 100644
Binary files a/docs/images/segment-y-override.png and b/docs/images/segment-y-override.png differ
diff --git a/docs/images/segment-y.png b/docs/images/segment-y.png
index e57ffba..15d46b0 100644
Binary files a/docs/images/segment-y.png and b/docs/images/segment-y.png differ
diff --git a/docs/images/separate-edge.png b/docs/images/separate-edge.png
new file mode 100644
index 0000000..4302449
Binary files /dev/null and b/docs/images/separate-edge.png differ
diff --git a/docs/images/vscrews-plate-edges.png b/docs/images/vscrews-plate-edges.png
index 04527c4..3b438fd 100644
Binary files a/docs/images/vscrews-plate-edges.png and b/docs/images/vscrews-plate-edges.png differ
diff --git a/docs/images/vscrews-segment-edges.png b/docs/images/vscrews-segment-edges.png
index 66a0762..a6a5e3b 100644
Binary files a/docs/images/vscrews-segment-edges.png and b/docs/images/vscrews-segment-edges.png differ
diff --git a/docs/images/wall-bottom.png b/docs/images/wall-bottom.png
index bf1a424..5a1a964 100644
Binary files a/docs/images/wall-bottom.png and b/docs/images/wall-bottom.png differ
diff --git a/docs/images/wall-top.png b/docs/images/wall-top.png
index 4b0d763..a95e284 100644
Binary files a/docs/images/wall-top.png and b/docs/images/wall-top.png differ
diff --git a/docs/images/whole.png b/docs/images/whole.png
index 3c58f99..1baf032 100644
Binary files a/docs/images/whole.png and b/docs/images/whole.png differ
diff --git a/editor.toml b/editor.toml
index 15c236a..73c284a 100644
--- a/editor.toml
+++ b/editor.toml
@@ -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"
diff --git a/gridflock.scad b/gridflock.scad
index c10d0f2..e511bbf 100644
--- a/gridflock.scad
+++ b/gridflock.scad
@@ -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] */
@@ -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
@@ -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
@@ -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() {
@@ -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;
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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.