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_ 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. + + +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. 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.