BelfrySCAD / BOSL2

The Belfry OpenScad Library, v2.0. An OpenSCAD library of shapes, masks, and manipulators to make working with OpenSCAD easier. BETA
https://github.com/BelfrySCAD/BOSL2/wiki
BSD 2-Clause "Simplified" License
1k stars 114 forks source link

Add hex_panel to the BOSL2 library #1381

Closed rjcarlson49 closed 5 months ago

rjcarlson49 commented 9 months ago

Proposal For my own library I've created hex_panel(), a module that produces a panel in any shape whose inkier is a honeycomb. I believe it would be a good addition to BOSL2. I've attached a full set of code here that is attachable and complies as best I can with BOSL2 norms. There is documentation and examples.

Background Since I started using OpenSCAD, I've liked using hexagonal structures for lightness and strength. And I think it often looks cool. I have gone through several iterations of my own code, but I was never really pleased with it. A few months ago someone posted an elegant little piece of code that imposed a honeycomb on any shape. I'm afraid I don't remember who posted it. It worked and I did not have the motivation to figure out how regions work, so I have just used it as it came to me.

There were various problems with that code, especially that it did not produce a panel with the expected dimensions. Because it used stroke, the width of the stroke was added to the shape. I began playing with it to produce something easier to use. This is the final result.

Features The main input is shape. Shape can be a 3D vector. Then you have a simple rectangular panel, that lies in the XY plane and z defines the thickness. The frame argument defines width of the solid outer frame of the panel.

Shape can also be a non crossing path that defines a polygon. Concave shapes are allowed. If shape is a path, the height of the panel must be given in the h argument.

Rectangular panels, i.e. shape is a 3D vector, can be beveled, always at 45 degrees. This makes it easy to combine panels cleanly into boxes or whatever. RIGHT, LEFT, FRONT and BACK represent the 4 possible edges. Add BOTTOM to an edge to make a reverse bevel. With some work, the bevel angle could be made an argument, but I don't see the usefulness.

I've started adding a 'help' feature to my library. When the first argument is "help", the module prints a synopsis and halts with an assert failure. If you don't care for that, delete it! I've found it to be a quick way to remind myself of argument lists.

Example Code

include <BOSL2/std.scad>;
include <BOSL2/rounding.scad>;

$fn = 180;
showRect = false;
showShapes = false;
showBevels = false;
showPoly = false;
showJoin = false;
showHelp = false;

if (showHelp) {
    hex_panel("help");
}
if (showShapes) {
    s0 = glued_circles(d=50, spread=50, tangent=30);
    s1 = circle(30);
    zdistribute(spacing = 20){
        hex_panel(s0, h = 10, frame = 5); 
        hex_panel(s1, h = 10, frame = 5);
    }
}
if (showPoly) {
    s2 = [[0, -40], [0, 40], [30, 20], [60, 40], [60, -40], [30, -20]];
    s1 = [[0, -40], [0, 40], [60, 0]];
    s0 = [[0, -40], [0, 70], [60, 0], [80, 20], [70, -20]];
    zdistribute(spacing = 20){
        hex_panel(s2, h = 10, frame = 5); 
        hex_panel(s0, h = 10, frame = 5); 
        hex_panel(s1, h = 10, frame = 5); 
    }
}
if (showJoin) {
    hex_panel([50, 100, 10], frame = 5, bevel = [FWD,  BACK], anchor = BACK + RIGHT + BOTTOM, orient = RIGHT);
    hex_panel([100, 50, 10], frame = 5, bevel = [LEFT, RIGHT], anchor = FWD + LEFT + BOTTOM, orient = FWD);
}
if (showRect) {
    zdistribute(spacing = 20){
        hex_panel([50, 100, 5], frame = 5);
        hex_panel([50, 100, 5], frame = 5, wall = 5);
        hex_panel([50, 100, 5], frame = 5, spacing = 20);
        hex_panel([50, 100, 5], frame = 10, spacing = 20, wall = 4);
    }
}
if (showBevels) {
    zdistribute(spacing = 20){
        hex_panel([50, 100, 10], frame = 5, bevel = []);
        hex_panel([50, 100, 10], frame = 5, bevel = [LEFT, RIGHT]);
        hex_panel([50, 100, 10], frame = 5, bevel = [FWD,  BACK]);
        hex_panel([50, 100, 10], frame = 5, bevel = [LEFT, RIGHT, FWD, BACK]);
        hex_panel([50, 100, 10], frame = 5, bevel = [LEFT, RIGHT, FWD+BOTTOM, BACK+BOTTOM]);
    }
}

t = .1;
t2 = 2 * t;
tiny = 1/128;

function _honeycomb(shape, spacing=10, hex_wall=1) =
    let(
        hex = hexagon(id=spacing-hex_wall, spin=180/6),
        bounds = pointlist_bounds(shape),
        size = bounds[1] - bounds[0],
        hex_rgn2 = grid_copies(spacing=spacing, size=size, stagger=true, p=hex),
        center = (bounds[0] + bounds[1]) / 2,
        hex_rgn = move(center, p=hex_rgn2),
        ihex_rgn = intersection(hex_rgn, shape),
        out_rgn = difference(shape, ihex_rgn)
    ) out_rgn;

module _honeycomb(shape, spacing=10, hex_wall=1) {
    rgn = _honeycomb(shape, spacing=spacing, hex_wall=hex_wall);
    region(rgn);
}

function _is_in(target, vector) = 
         let(r = search([target], vector)) 
         len(r) == 0 ? false : r[0] == [] ? false : true;

// Module: hex_panel()
// Usage:
//   hex_panel(shape, frame, [h,] [wall = 1.5,] [spacing = 10,] [bevel,] [anchor = CENTER,] [orient = UP,] [spin = 0])
// Description:
//   Produces a panel with a honeycomb interior. The panel consists of a frame containing
//   a honeycob interior. The frame is laid out in the XY plane with the honeycob interior 
//   and then extruded to the height h. The shape argument defines the outer bounderies of
//   the frame.
//   .
//   The simplest way to define the frame shape is to give a 3D vector in the shape argument.
//   In this case, x and y define a rectangle which is the frame and z is the height (h). 
//   The h argument is ignored in this case.
//   . 
//   The other option is to provide a 2D path as the shape argument. The path must be 
//   non-crossing and is assumed to be closed. 
//   .
//   Enter "help" as the shape argument to echo the synopsis to the console.
//   .
// Arguments:
//   shape = either a path as defined in BOSL2 or a 3D vector in the form [x, y, z]. 
//   if shape == "help" an assert will fail and a synopsis of the module is printed.
//   frame = width of the frame around the honeycomb, required
//   h = thickness of the panel, required when shape is a path.
//   ---
//   wall = thickness of the walls in the honeycomb, Default: 1.5
//   spacing = size of the hex cells in the honeycomb: Default: 10
//   bevel = vector of vectors. Each vector entry must be one of the BOSL2 directional
//   defines RIGHT, LEFT, BACK, or FRONT. BOTTOM may be added to produce a bevel
//   on the bottom of the panel, as in BACK+BOTTOM. Entries MUST be separated by commas.
//   Bevel is allowed only when shape is a simple 3D vector, [x, y, z]. Default: [].
//   anchor = the usual BOSL2 anchors
//   orient = the usual BOSL2 orientations
//   spin = the usual BOSL2 spin
// Example: Rectangular Panels
//   zdistribute(spacing = 20){
//     hex_panel([50, 100, 5], frame = 5);
//     hex_panel([50, 100, 5], frame = 5, wall = 5);
//     hex_panel([50, 100, 5], frame = 5, spacing = 20);
//     hex_panel([50, 100, 5], frame = 10, spacing = 20, wall = 4);
//   }
// Example: Bevel Combinations
//   zdistribute(spacing = 20){
//     hex_panel([50, 100, 10], frame = 5, bevel = []);
//     hex_panel([50, 100, 10], frame = 5, bevel = [LEFT, RIGHT]);
//     hex_panel([50, 100, 10], frame = 5, bevel = [FWD,  BACK]);
//     hex_panel([50, 100, 10], frame = 5, bevel = [LEFT, RIGHT, FWD, BACK]);
//     hex_panel([50, 100, 10], frame = 5, bevel = [LEFT, RIGHT, FWD+BOTTOM, BACK+BOTTOM]);
//  }
// Example: Shapes
//   s0 = glued_circles(d=50, spread=50, tangent=30);
//   s1 = circle(30);
//   zdistribute(spacing = 20){
//     hex_panel(s0, h = 10, frame = 5); 
//     hex_panel(s1, h = 10, frame = 5);
//   }

module hex_panel(
    shape,
    frame, 
    h,
    wall = 1.5, 
    spacing = 10,
    bevel = [],
    anchor = CENTER, 
    orient = UP, 
    spin = 0) 
{
    //d00 = echo("Frame ", frame, "  h ", h, "  shape ", shape);
    d0 = assert(shape!="help", "\n\nhex_panel(shape, frame, [h,] \n    [wall = 1.5,] [spacing = 10,] [bevel,]\n    [anchor = CENTER], [orient = UP,] [spin = 0])\n\n");

    assert(!is_undef(frame) && frame > 0, "frame must be > 0");
    assert(is_path(shape) || is_vector(shape, 3), "shape must be a path or a 3D vector");
    assert(len(bevel) == 0 || is_vector(shape, 3), "bevel must be used only on rectangular panels");
    assert(is_path(shape) || (shape.x > 0 && shape.y > 0 && shape.z > 0), 
           "shape must be a path or a 3D vector");
    assert(!(_is_in(FRONT, bevel) && _is_in(FRONT+BOTTOM, bevel)), "conflicting FRONT bevels");
    assert(!(_is_in(BACK,  bevel) && _is_in(BACK+BOTTOM,  bevel)), "conflicting BACK bevels");
    assert(!(_is_in(RIGHT, bevel) && _is_in(RIGHT+BOTTOM, bevel)), "conflicting RIGHT bevels");
    assert(!(_is_in(LEFT,  bevel) && _is_in(LEFT+BOTTOM,  bevel)), "conflicting LEFT bevels");
    shp = is_path(shape) ? shape : square([shape.x, shape.y], center = true);
    ht = is_path(shape) ? h : shape.z;

    //d1 = echo("is_path(shape) ", is_path(shape), " Shape ", shp);

    bounds = pointlist_bounds(shp);
    sizes = bounds[1] - bounds[0]; // [xsize, ysize]
    //d2 = echo("sizes, h ", sizes, ht);
    centers = (bounds[0] + bounds[1]) / 2;

    assert(frame*2 + spacing < sizes[0], "There must be room for at least 1 cell in the honeycomb");
    assert(frame*2 + spacing < sizes[1], "There must be room for at least 1 cell in the honeycomb");

    attachable(anchor = anchor, spin = spin, orient = orient, size = [sizes.x, sizes.y, ht]) {
        //d = echo("Bounds, Centers, Sizes ", bounds, centers, sizes);
        if (len(bevel) > 0) {
            intersection() {
                union() {
                    left(centers.x) fwd(centers.y) down(ht/2) 
                    linear_extrude(height = ht) {
                        _honeycomb(shp, spacing = spacing, hex_wall = wall);

                        offset_stroke(shp, width=[-frame, 0], closed=true);
                    }

                    for (b = bevel) _bevelWall(shape, b);
                }
                down(ht/2)
                _bevelSolid(shape, bevel);
            }
        } else {
            left(centers.x) fwd(centers.y) down(ht/2) 
            linear_extrude(height = ht) {
                _honeycomb(shp, spacing = spacing, hex_wall = wall);

                offset_stroke(shp, width=[-frame, 0], closed=true);
            }
        }
        children();
    } // attachable
}

module _bevelSolid(shape, bevel) {
    tX = _is_in(RIGHT,          bevel) ? -shape.z : 0;
    tx = _is_in(LEFT,           bevel) ?  shape.z : 0;
    tY = _is_in(BACK,           bevel) ? -shape.z : 0;
    ty = _is_in(FRONT,          bevel) ?  shape.z : 0;
    bX = _is_in(RIGHT + BOTTOM, bevel) ? -shape.z : 0;
    bx = _is_in(LEFT  + BOTTOM, bevel) ?  shape.z : 0;
    bY = _is_in(BACK  + BOTTOM, bevel) ? -shape.z : 0;
    by = _is_in(FRONT + BOTTOM, bevel) ?  shape.z : 0;

    pathB = [[ shape.x/2 + bX,  shape.y/2 + bY], [ shape.x/2 + bX, -shape.y/2 + by], 
             [-shape.x/2 + bx, -shape.y/2 + by], [-shape.x/2 + bx,  shape.y/2 + bY]];
    pathT = [[ shape.x/2 + tX,  shape.y/2 + tY], [ shape.x/2 + tX, -shape.y/2 + ty], 
             [-shape.x/2 + tx, -shape.y/2 + ty], [-shape.x/2 + tx,  shape.y/2 + tY]];

    hull() {
        down(tiny) linear_extrude(tiny) polygon(pathB);
        up(shape.z-tiny) linear_extrude(tiny) polygon(pathT);
    }
}

module _bevelWall(shape, bevel) {

    l = bevel.y != 0 ? shape.x : shape.y;
    d = bevel.y != 0 ? shape.y : shape.x;
    zr = bevel.y == -1 ? 180 
       : bevel.y ==  1 ? 0 
       : bevel.x == -1 ? 90 
       : bevel.x ==  1 ? 270 
       : undef;
    xr = bevel.x != 0 && bevel.z < 0 ? 180 : 0;
    yr = bevel.y != 0 && bevel.z < 0 ? 180 : 0;

    path = [[-1, 0], [0, 0], [-shape.z, -shape.z], [-shape.z-1, -shape.z]];

    xrot(xr) 
    yrot(yr)
    zrot(zr)
    down(shape.z/2) 
    back(d/2) 
    right(l/2) 
    zrot(90) 
    xrot(-90)
    linear_extrude(l) polygon(path);
}

Rectangular panels with various bevel combinations.

bevels
adrianVmariano commented 9 months ago

This seems like it could go in walls.scad. Probably needs to be named hex_wall() for consistency with the others.

A few minor things:

Does _is_in() do something different from BOSL2 in_list()? Assertions have some redundancy and you should always put assertions into an assignment statement, so write your stack of assertions as

dummy = assert() assert() assert() ... assert();

Otherwise assertions run after assignments and you get errors even though you thought you trapped bad input with the assertions. You might want to use all_positive().

For formatting the doc string, the arguments are one argument per line, so if the description of an argument is long you just have to still put it all on one line. Shape is described as "a path". Can it be a non-coplanar 3d path? Or must it be a 2d path? Could you make pathB and pathT with calls to rect()?

We are not going to add usage texts to every module with "help" argument, so that feature will need to go.

rjcarlson49 commented 9 months ago

This seems like it could go in walls.scad. Probably needs to be named hex_wall() for consistency with the others.

Got no problem with hex_wall.

Does _is_in() do something different from BOSL2 in_list()?

Probably. Search has annoying properties so I put in _is_in(). If in_list() is part of std BOSL2 I'll start using it.

Assertions have some redundancy and you should always put assertions into an assignment statement, so write your stack > of assertions as

dummy = assert() assert() assert() ... assert();

Noted, I did not realize that asserts could be juts concatenated.

For formatting the doc string, the arguments are one argument per line, so if the description of an argument is long you just have to still put it all on one line.

Noted

Shape is described as "a path". Can it be a non-coplanar 3d path? Or must it be a 2d path?

2D only I'm pretty sure.

Could you make pathB and pathT with calls to rect()?

Probably. I did not think through whether the TOP & BOTTOM solid faces are always rectangles.

rjcarlson49 commented 9 months ago

I verified that _is_in() can be replaced with in_list() with no other changes, just a replace.

rjcarlson49 commented 8 months ago

I reported the F5 render bug that appears here to openScad. I believe it's only a manifold issue. Adding convexity = 8 to the linear_extrude avoids the bug.

adrianVmariano commented 8 months ago

Did you list this in the wrong place? Note that if by "bug" you mean that some surfaces are missing in F5 preview when you examine the model, and this is fixed by convexity=8, then that's not a bug. That's just what happens when you don't give a large enough convexity value.

Do you have an updated version of the hex walls based on comments above?

rjcarlson49 commented 8 months ago

No, not the wrong place, I was just letting you know so that you could update the code. Just change "linear_extrude(height = ht) {" to "linear_extrude(height = ht, convexity = 8) {" in the main module.

I was led to believe that it is a bug, but avoided by adding the argument. I don't much care about it since it does not affect F6 and I rarely use F5. you can view the scad "bug" here, https://github.com/openscad/openscad/issues/5010.

adrianVmariano commented 8 months ago

I'm not sure why you were lead to believe it is a bug. It's not a bug---it's just how preview works.

rjcarlson49 commented 8 months ago

I have been using this and when I went to use the shape feature I realized there is a problem with how the bounds and center are calculated when it is not a rectangle. First thought is that the shape should first be centered on it's centroid, then bounds taken from that point.

rjcarlson49 commented 8 months ago

replacing the two lines centers = (bound[0] + bounds[1]) / 2; and center = = (bound[0] + bounds[1]) / 2;

with

centers = centroid(shp); and center = centroid(shape);

seems to fix the problem.

adrianVmariano commented 8 months ago

Wonder if it would make sense to anchor the general polygon case as a linear extrusion. That would give different centering options.

rjcarlson49 commented 8 months ago

That might well be. I haven't looked at the anchoring case for linear extrusions, so I couldn't guess at implications.

adrianVmariano commented 8 months ago

It anchors to the polygon, or up/down from its center (which can be defined as centroid, mean, or bounding box). The problem is that for arbitrary polygons, it's tough to produce a anchoring scheme that always produces a reasonable result. But your shape is in fact a linear extrusion of a polygon, right? (In the polygon case.)

adrianVmariano commented 6 months ago

This is in the latest PR. Ended up being quite a few changes, so check the docs. If you want to reproduce the old behavior set bevel_frame=1. (This was a hard coded parameter.)

rjcarlson49 commented 5 months ago

Thanks. It’ll be a little while till I can look. I’m in Victoria Falls on a safari trip!

-Bob Tucson AZ

On May 18, 2024, at 05:40, adrianVmariano @.***> wrote:

This is in the latest PR. Ended up being quite a few changes, so check the docs. If you want to reproduce the old behavior set bevel_frame=1. (This was a hard coded parameter.)

— Reply to this email directly, view it on GitHub https://github.com/BelfrySCAD/BOSL2/issues/1381#issuecomment-2118623576, or unsubscribe https://github.com/notifications/unsubscribe-auth/ANJ3IKDG552LL7KIBT3MACTZC3ERJAVCNFSM6AAAAABDMXOAY2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMJYGYZDGNJXGY. You are receiving this because you authored the thread.