Skip to content

Any interest in a D-Ring module? #1687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dan-p3rry opened this issue May 15, 2025 · 29 comments
Open

Any interest in a D-Ring module? #1687

dan-p3rry opened this issue May 15, 2025 · 29 comments
Assignees

Comments

@dan-p3rry
Copy link

Image

I wrote a module to create D-rings. It's quite simple, a union of a rounded_prism() and a ycyl(). It's only interesting because the tangents are calculated so that any reasonably sized base can join to any ycyl. If you are interested, I will add some error checking and share the code.

@adrianVmariano
Copy link
Collaborator

Seems like there is interest. Hardest thing to figure out about this is probably how to handle the attachment settings. Do you attach to the bounding box cube? It's also not a D-ring, so maybe the name should be something else?

Any thoughts on where it belongs in the library?

@adrianVmariano
Copy link
Collaborator

Do you support a D-ring form where you only have a half-circle?

@dan-p3rry
Copy link
Author

Seems like there is interest. Hardest thing to figure out about this is probably how to handle the attachment settings. Do you attach to the bounding box cube? It's also not a D-ring, so maybe the name should be something else?

My BOSL2 skills are not to the point where I use attachments, so I have no idea .... I call it a "d_shape".

Any thoughts on where it belongs in the library?

shapes3d.scad, "other round objects"

Do you support a D-ring form where you only have a half-circle?

Are you asking about the overall shape, or the through-hole? The overall shape can approach a half circle, as long as the rounded_prism() shape has a finite height. The current code for the through hole only supports a full cylinder.

Image

@adrianVmariano
Copy link
Collaborator

I was asking about the hole---you know, to make it an actual D-ring.

The thinking about this is that it's a "part" rather than a "shape" so it would belong somewhere in the parts library. But I'm not sure what it would be called. (Coming up with names for things in the library is often a challenge.) What name would convey its use and function, so that somebody would find it if they needed something like this?

I encourage you to read through the attachments tutorial https://github.com/BelfrySCAD/BOSL2/wiki/Tutorial-Attachments and learn about attachments, because I think they are possibly the most important thing in the library for how they can simplify a model made from more than one object.

@dan-p3rry
Copy link
Author

I tried to modify this code:

        union() {
            rounded_prism(rect([b_width, thickness]), 
                rect([2*tangent.x, thickness]), height=tangent.y, 
                joint_bot=roundingb, joint_sides=rounding, k_sides=0.92,
                anchor=BOTTOM);
            top_half(s=2.1*max(b_width, 2*d_rad, thickness), z=tangent.y)
                up(b_height)  
                    ycyl(r=d_rad, h=thickness, rounding=rounding);
        }

to use attachments:

        rounded_prism(rect([b_width, thickness]), 
            rect([2*tangent.x, thickness]), height=tangent.y, 
            joint_bot=roundingb, joint_sides=rounding, k_sides=0.92,
            anchor=BOTTOM)
        attach(TOP, CENTER, overlap=tangent.y - b_height)
            top_half(p=cyl(r=d_rad, h=thickness, rounding=rounding, 
                    orient=FRONT), s=2.1*max(b_width, 2*d_rad, thickness), 
                    z=tangent.y);

but it fails with an error

WARNING: variable "p" not specified as parameter in file d_shape.scad, line 48

It works without top_half(), but I tried several different things and never got it to work with top_half(). Any suggestions?

@adrianVmariano
Copy link
Collaborator

I can't actually run the code to see what you're doing because it's got a bunch of undefined variables.

But in your second example, you're using top_half as a module but passing it a p argument, which only exists for the function form, because the p argument replaces the child that goes to the module. So for the module it needs to be top_half(s=..., z=...) cyl(....).

There is another issue, which is that top_half is going to lose the geometry info (because we can't get geometry out of modules) so attachment won't work properly there. If you want to do that operation you'll need to attach the full cylinder and cut it off after attaching it.

Note that making your object attachable is separate from using attachments to make your object. To do that you need to decide what the correct anchors are for your object and invoke attachable() when your object makes its geometry.

@dan-p3rry
Copy link
Author

dan-p3rry commented May 19, 2025

Following code includes attachments, but attachment are only correct in Z-dimension when b_height = d_rad. The part is non-symmetric to the center of the "D" circle when b_height != d_rad and then anchors are incorrect. I don't know how to fix this. Since this part is non-symmetrical in Z, it might make more sense to anchor the part to the X-Y plane, but I couldn't figure out those anchors either.

Also, I will take a look at allowing a D-shaped hole. Are you thinking the D-shaped hole would be a semicircle? Otherwise we are adding several new variables ....

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

$fs = 0.4; 
$fa = 1;

b_width = 50;   // base width: width of the flat part of the "D"
d_rad = 25;     // D radius: radius of the round part of the "D"
b_height = 25;  // base height: distance from the flat part of "D" 
                // to center of the round part of the "D"
thickness = 12; // Y dimension of the part
rounding = 6;   // rounding of "D"
roundingb = -5; // base rounding
holeR = 0;      // hole placed in center of "D" when > 0
roundingh = 6;  // rounding of the center hole

d_shape(b_width, d_rad, b_height, thickness=thickness, 
        rounding=rounding, roundingb=roundingb, 
        holeR=holeR, roundingh=roundingh) show_anchors();

// right(80)
// d_shape(70, 25, 1, thickness=thickness, 
//         rounding=0, roundingb=0, 
//         holeR=0, roundingh=roundingh);

// right(160)
// d_shape(46, 25, 10, thickness=thickness, 
//         rounding=3, roundingb=0, 
//         holeR=0, roundingh=0);

module d_shape(b_width, d_rad, b_height, thickness, holeR=0, 
            rounding=0, roundingb=0, roundingh=0,
            anchor=BOTTOM, spin=0, orient=UP) {

    assert(sqrt((0.5*b_width)^2 + b_height^2)>d_rad, 
        "[0.5*b_width, 0] must be outside the circle of radius d_rad centered at [0, b_height]");
    // I cannot imagine anyone would ever want rounding or roundingh
    // to be < 0, but should be OK

    tangents = circle_point_tangents(r=d_rad, cp=[0, b_height], 
                pt=[0.5*b_width, 0]);
    // we want the tangent with the larger y value
    tangent = tangents[0].y > tangents[1].y
            ? tangents[0] : tangents[1];

    attachable(anchor, spin, orient, size=[max(b_width,2*d_rad), 
            thickness, b_height+d_rad]) {
        difference() {
            union() {
                top_half(s=2.1*max(b_width, 2*d_rad, thickness), 
                            z=tangent.y - b_height)
                    ycyl(r=d_rad, h=thickness, rounding=rounding);
                up(tangent.y - b_height)
                rounded_prism(rect([b_width, thickness]), 
                    rect([2*tangent.x, thickness]), height=tangent.y, 
                    joint_bot=roundingb, joint_sides=rounding, 
                    k_sides=0.92, anchor=TOP
                    );
            }
            if (holeR > 0)
                ycyl(r=holeR, h=thickness + 0.01, rounding=-roundingh);

        } // difference
        children();
    } // attachable
}

@adrianVmariano
Copy link
Collaborator

The way attachable works, the children need to be centered in every direction. Your asymmetric shapes where anchoring fails are not centered vertically. Basically when you give the size, it's defining a centered cuboid with that size as the anchor points. If you then make BOTTOM the default anchor, the shape will appear sitting on the XY plane.

I think the way you have it with the attachment point DOWN is the right way to do this. (Note also that if you turned it sideways the anchors would still be wrong if you didn't center it.)

Regarding a D shaped hole, only if you think it's useful and you want to do it. I toss out ideas and they aren't necessarily always good, so pushback/dialog is fine. You may want to consider talking about contributions on the developer chat (link on the github landing page).

Why would we want a D hole? Well, a D hole lets you have a wider slot in a shorter height. You could address the need for a wide slot by making the hole an ellipse, which just requires one parameter and would possibly be a trivial modification. That also avoids the problematic issue of rounding the inside corners of the D. Another wide hole option might be a rounded rectangle of some kind or the squircle.

For naming it seems like this object is like an eye bolt without the bolt. The front running name so far is connector_ring() which I'm not a big fan of, so still looking for ideas there.

@dan-p3rry
Copy link
Author

OK, I think I'm done. Anchors are adjusted to fit the upper curve, plus custom anchors added. A D-shaped hole is supported, too.

Image

Some ideas for future enhancement: support chamfer & for the base, teardrop mask; the D-shape could be ellipse-shaped instead of strictly circular.

@dan-p3rry
Copy link
Author

Here's the code, including the module calls for the examples above. Let me know if you find any bugs.

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

$fs = 0.4; 
$fa = 1;

base_w = 40;   // base width: width of the flat part of the "D"
d_rad = 25;     // D radius: radius of the round part of the "D"
base_h = 25;  // base height: distance from the flat part of "D" 
                // to center of the round part of the "D"
thickness = 12; // Y dimension of the part
rounding = 0;   // rounding of "D"
roundingb = 0; // base rounding
holeR = 15;      // hole placed in center of "D" when > 0
roundingh = 2;  // rounding of the center hole

d_shape(base_w, base_h, d_rad, thickness=thickness, 
        rounding=rounding, roundingb=roundingb, 
        holeR=holeR, roundingh=roundingh, anchor=BOTTOM)
        show_anchors()
        ;

right(100)
d_shape(50, 10, 25, thickness=thickness, 
        rounding=5, roundingb=0, 
        holeR=15, holeD_Bool=true, roundingh=5);

right(200)
d_shape(50, 40, 25, thickness=thickness, 
        rounding=3, roundingb=0, 
        holeR=0, roundingh=0) show_anchors();

module d_shape(base_w, base_h, d_rad, thickness, holeR=0, holeD_Bool=false,
            rounding=0, roundingb=0, roundingh=0,
            anchor=BOTTOM, spin=0, orient=UP) {

    assert(sqrt((0.5*base_w)^2 + base_h^2)>d_rad, 
        "[0.5*base_w, 0] must be outside the circle of radius d_rad centered at [0, base_h]");
    max_holeR = holeR > 0 && roundingb < 0 ? base_h + roundingb
                                : base_h;
    if (holeR > 0 && !holeD_Bool) assert(holeR + roundingh < max_holeR, 
                "holeR + roundingh must be less than max_holeR");
    assert(roundingh >= 0, "roundingh must be greater than or equal to 0");
    // I cannot imagine anyone would ever want rounding 
    //      to be < 0, but allowed for now

    z_offset = 0.5*(base_h - d_rad);
    tangents = circle_point_tangents(r=d_rad, cp=[0, base_h], 
                pt=[0.5*base_w, 0]);
    // we want the tangent with the larger y value
    tangent = tangents[0].y > tangents[1].y
            ? tangents[0] : tangents[1];
    // anchor calcs
    angle = atan((tangent.x - 0.5*base_w)/tangent.y);
    top_x = 0.5*base_w + (base_h + d_rad)*tan(angle);
    // when d_rad > 0.5*base_w, need to move the anchor
    // use x^2 + y^2 = r^2, x = sqrt(r^2 - y^2)
    delta_y = z_offset;
    mid_x = sqrt(d_rad^2 - delta_y^2);

    anchors = [
        named_anchor("hole_ctr_front", [0, -thickness/2, z_offset], FRONT, 0),
        named_anchor("hole_ctr_back", [0, thickness/2, z_offset], BACK, 0),
        named_anchor("tangent_right", [tangent[0], 0, tangent[1] - base_h + z_offset], RIGHT, 0),
        named_anchor("tangent_left", [-tangent[0], 0, tangent[1] - base_h + z_offset], LEFT, 0),
    ];
    override = [
        for (i = [-1, 1], j=[-1:1], k=[0:1])
            if (k==0 && j!=0 && d_rad > 0.5*base_w)
                [[i, j, 0], 
                [mid_x*unit([i, 0, 0]) + 0.5*thickness*unit([0, j, 0])]]
            else if (k==0 && d_rad > 0.5*base_w) 
                [[i, 0, 0], [mid_x*unit([i, 0, 0])]]
            else if (k==1 && j==0) 
                [[i, 0, 1], [d_rad*sin(45)*unit([i, 0, 0]) 
                            + (z_offset + d_rad*sin(45))*unit([0, 0, k])]]
            else if (k==1)
                [[i, j, 1], [d_rad*sin(45)*unit([i, 0, 0]) 
                                + 0.5*thickness*unit([0, j, 0])
                                + (z_offset + d_rad*sin(45))*unit([0, 0, k])]]
    ];

    attachable(anchor, spin, orient, 
                size=[base_w, thickness, base_h + d_rad],
                size2=[2*top_x, thickness],
                anchors=anchors, override=override) {
        up(z_offset) difference() {
            union() {
                top_half(s=2.1*max(base_w, 2*d_rad, thickness), 
                            z=tangent.y - base_h)
                    ycyl(r=d_rad, h=thickness, rounding=rounding);
                up(tangent.y - base_h)
                    rounded_prism(rect([base_w, thickness]), 
                        rect([2*tangent.x, thickness]), h=tangent.y, 
                        joint_bot=roundingb, joint_sides=rounding, 
                        k_sides=0.92, k_bot=0.92, anchor=TOP);
            }
            if (holeR > 0) {
                top_half(s=2.1*max(2*holeR, thickness), 
                            z = holeD_Bool ? 0 : -holeR - roundingh - 1)
                    ycyl(r=holeR, h=thickness + 0.01, 
                            rounding=-roundingh);
                if(roundingh > 0 && holeD_Bool) {
                    rpath = path_merge_collinear( concat(
                        path2d(arc(cp=[holeR, 0], r=roundingh, start=0, angle=-90)),
                        [[holeR, -roundingh], [-holeR, -roundingh], ],
                        path2d(arc(cp=[-holeR, 0], r=roundingh, start=270, angle=-90)),
                    ));
                    for (m = [0, 1]) mirror([0, m, 0])
                        back(0.5*thickness)
                        // fwd(20)
                        // stroke(rpath);
                        xrot(90) path_sweep(
                            mask2d_roundover(r=roundingh, 
                                    anchor=RIGHT), rpath);
                }
            }   // holeR
        } // difference
        children();
    } // attachable
}

@adrianVmariano
Copy link
Collaborator

From your examples it's looking good.

Can you write the doc text. Just take a look at some module in the library and follow the same form. The one thing that might not be obvious is in the Arguments section, the "---" separator separates between positional arguments, listed first, and named arguments. The docs processor uses indentation to parse stuff, so pay attention to indentation. You should create enough examples to display the significantly distinct modes of operation and various features.

@dan-p3rry
Copy link
Author

Did we get consensus on the name of the part? I saw the discussion, and I do agree that "attach" should not be part of the name. Other than that I have no strong feelings about any of the proposals.

@adrianVmariano
Copy link
Collaborator

Go with connector_ring for now. We can change it if some better idea comes along before we push the release. We also don't know which file it's going in. It should be categorized as a "part" but doesn't obviously fit in anywhere.

@dan-p3rry
Copy link
Author

I should have the documentation ready later today, maybe tomorrow. How can I test it to make sure the doc looks correct in the wiki page?

@adrianVmariano
Copy link
Collaborator

In principle you can install the docsgen package that creates the docs and run it locally. I haven't done this myself and hence can't tell you how to do it. Basically once it appears on the wiki...I look at it...and fix all the things that are wrong. I'll look over your docs for any obvious issues and then we'll see how it looks.

@dan-p3rry
Copy link
Author

Go with connector_ring for now.

After a bit more thought, I realized that we're looking at this part a little differently. For me, the through hole is just an extra option, and could even be removed from the module (and use the documentation to show how easy it is to add the through hole).
By calling the part a 'ring', you are assuming the part always has a hole through it. TBH, I cannot think of a use without a hole, the difference being that in real usage from my own experience, I would create the D-shape, then separately add stepped holes, such as a screw head + shaft, or a heat insert + screw hole.

@adrianVmariano
Copy link
Collaborator

The name should ideally be chosen so that people who want the thing will realize that it fits their needs and is the thing they want. It seems to me that the functional purpose of this thing is to put a ring on something. With an edge case option to turn off the hole, presumably because you're adding your own hole of some kind. I'm not compelled that calling it "connector_ring" is a bad choice or that it misleads---unless you can develop a major use case for the hole-free version.

@dan-p3rry
Copy link
Author

Here you go, with the documentation text. There are a few blanks to fill in .....

// Module: connector_ring()
// Synopsis: Build a part combining a y-cylinder with a prismoid, generally used to anchor the cylinder to a flat surface
// SynTags: Geom
// Topics: 
// See Also: 
// Usage:
//   connector_ring(base_w, base_h, d_rad, thickness, [holeR=], [holeD_Bool=], [rounding=], [roundingb=], [roundingh=], [anchor=], [spin=], [orient=])
// Description:
//   Combines a prismoid polyhedron (front and back faces parallel, top face centered over bottom face)
//   with the upper portion of a Y-cylinder to form an attachable ring.  Optionally a circular or semicircular
//   hole, sharing the same center of rotation as the larger Y-cylinder. Three rounding variables are used
//   to customize the part.  Base rounding will typically be negative, to attach to a flat surface.  The
//   edge rounding and hole rounding should be positive.
//   .
//   Some constraints to be aware of:
// Arguments:
//   base_w = base width, x-dimension of the flat bottom surface
//   base_h = base height, z-dimension from the base to the center of the Y-cylinder
//   d_rad = radius of the Y-cylinder
//   thickness = y-dimension of the part
//   ---
//   holeR = optional radius of the center hole
//   holeD_Bool = Boolean, set true to make the center hole to be semicircular.  False makes the hole to be circular.
//   rounding = rounding of the vertical-ish edges of the prismoid and the exposed edges of the Y-cylinder
//   roundingb = base rounding, typically negative value
//   roundingh = rounding of the hole
//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
//   spin = Rotate this many degrees around the Z axis.  See [spin](attachments.scad#subsection-spin).  Default: `0`
//   orient = Vector to rotate top towards.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
// Named anchors:
//   hole_ctr_front = front, center of the Y-cylinder (same as the part FRONT+CENTER if base_h=d_rad)
//   hole_ctr_back = back, center of the Y-cylinder (same as the part BACK+CENTER if base_h=d_rad)
//   tangent_right = right side anchor at the point where the prismoid merges with Y-cylinder, at y=0
//   tangent_left = left side anchor at the point where the prismoid merges with Y-cylinder, at y=0
// Example: Basic usage
//   connector_ring(50, 25, 25, 10);
// Example: Widen the base, add base rounding
//   connector_ring(60, 25, 25, 10, roundingb=-3);
// Example: Narrow base.  The corners of the base must be outside the Y-cylinder in order to calculate a tangent between the prismoid and cylinder.
//   connector_ring(40, 20, 25, 10);
//   up(20) color("blue", 0.25) ycyl(r=25, h=11);
//   right(20) color("red") ycyl(r=1, h=11);
// Example: Add a through-hole with rounding on the hole edge
//   connector_ring(50, 40, 25, 10, holeR=20);
// Example: base_h must be greater than 0;
//   connector_ring(50, 0.1, 25, 10);
// Example: When using rounding with holeR>0, base_h must be greater than holeR + roundingh (+abs(roundingb) if roundingb < 0)
//   connector_ring(50, 26.1, 25, 10, holeR=20, roundingh=3, roundingb=-3);
// Example: Rounding all edges
//   connector_ring(50, 40, 25, 10, holeR=15, rounding=5, roundingh=5, roundingb=-5);
// Example: Semi-circular through hole with holeD_Bool = true
//   connector_ring(50, 12, 25, 10, holeR=15, holeD_Bool=true, rounding=5, roundingh=5, roundingb=-5);
// Example: The connector_ring includes 4 custom anchors in addition to the standard set: front & back at the center of the Y-cylinder component and left & right at the tangent points.
//   connector_ring(50, 25, 25, 10) show_anchors();
// Example: Use the custom anchor to place a screw hole
//    include <BOSL2/screws.scad>
//    connector_ring(20, 15, 7, 10, roundingb=-3) 
//       attach("hole_ctr_front") 
//         #screw_hole("M5", length=20, head="socket", atype="head", anchor=TOP, orient=UP);


module connector_ring(base_w, base_h, d_rad, thickness, holeR=0, holeD_Bool=false,
            rounding=0, roundingb=0, roundingh=0,
            anchor=BOTTOM, spin=0, orient=UP) {

    assert(sqrt((0.5*base_w)^2 + base_h^2)>d_rad, 
        "Point [0.5*base_w, 0] must be outside the circle of radius d_rad centered at [0, base_h]");
    max_holeR = holeR > 0 && roundingb < 0 ? base_h + roundingb
                                : base_h;
    if (holeR > 0 && !holeD_Bool) assert(holeR + roundingh < max_holeR, 
                "holeR + roundingh must be less than max_holeR");
    assert(roundingh >= 0, "roundingh must be greater than or equal to 0");
    assert(d_rad > holeR, "Part radius must be larger than the through hole");
    // I cannot imagine anyone would ever want rounding 
    //      to be < 0, but allowed for now

    z_offset = 0.5*(base_h - d_rad);
    tangents = circle_point_tangents(r=d_rad, cp=[0, base_h], 
                pt=[0.5*base_w, 0]);
    // we want the tangent with the larger y value
    tangent = tangents[0].y > tangents[1].y
            ? tangents[0] : tangents[1];
    // anchor calcs
    angle = atan((tangent.x - 0.5*base_w)/tangent.y);
    top_x = 0.5*base_w + (base_h + d_rad)*tan(angle);
    // when d_rad > 0.5*base_w, need to move the anchor
    // use x^2 + y^2 = r^2, x = sqrt(r^2 - y^2)
    delta_y = z_offset;
    mid_x = sqrt(d_rad^2 - delta_y^2);

    anchors = [
        named_anchor("hole_ctr_front", [0, -thickness/2, z_offset], FRONT, 0),
        named_anchor("hole_ctr_back", [0, thickness/2, z_offset], BACK, 0),
        named_anchor("tangent_right", [tangent[0], 0, tangent[1] - base_h + z_offset], RIGHT, 0),
        named_anchor("tangent_left", [-tangent[0], 0, tangent[1] - base_h + z_offset], LEFT, 0),
    ];
    override = [
        for (i = [-1, 1], j=[-1:1], k=[0:1])
            if (k==0 && j!=0 && d_rad > 0.5*base_w)
                [[i, j, 0], 
                [mid_x*unit([i, 0, 0]) + 0.5*thickness*unit([0, j, 0])]]
            else if (k==0 && d_rad > 0.5*base_w) 
                [[i, 0, 0], [mid_x*unit([i, 0, 0])]]
            else if (k==1 && j==0) 
                [[i, 0, 1], [d_rad*sin(45)*unit([i, 0, 0]) 
                            + (z_offset + d_rad*sin(45))*unit([0, 0, k])]]
            else if (k==1)
                [[i, j, 1], [d_rad*sin(45)*unit([i, 0, 0]) 
                                + 0.5*thickness*unit([0, j, 0])
                                + (z_offset + d_rad*sin(45))*unit([0, 0, k])]]
    ];

    attachable(anchor, spin, orient, 
                size=[base_w, thickness, base_h + d_rad],
                size2=[2*top_x, thickness],
                anchors=anchors, override=override) {
        up(z_offset) difference() {
            union() {
                top_half(s=2.1*max(base_w, 2*d_rad, thickness), 
                            z=tangent.y - base_h)
                    ycyl(r=d_rad, h=thickness, rounding=rounding);
                up(tangent.y - base_h)
                    rounded_prism(rect([base_w, thickness]), 
                        rect([2*tangent.x, thickness]), h=tangent.y, 
                        joint_bot=roundingb, joint_sides=rounding, 
                        k_sides=0.92, k_bot=0.92, anchor=TOP);
            }
            if (holeR > 0) {
                top_half(s=2.1*max(2*holeR, thickness), 
                            z = holeD_Bool ? 0 : -holeR - roundingh - 1)
                    ycyl(r=holeR, h=thickness + 0.01, 
                            rounding=-roundingh);
                if(roundingh > 0 && holeD_Bool) {
                    rpath = path_merge_collinear( concat(
                        path2d(arc(cp=[holeR, 0], r=roundingh, start=0, angle=-90)),
                        [[holeR, -roundingh], [-holeR, -roundingh], ],
                        path2d(arc(cp=[-holeR, 0], r=roundingh, start=270, angle=-90)),
                    ));
                    for (m = [0, 1]) mirror([0, m, 0])
                        back(0.5*thickness) xrot(90) 
                        path_sweep(
                            mask2d_roundover(r=roundingh, 
                                    anchor=RIGHT), rpath);
                }
            }   // holeR
        } // difference
        children();
    } // attachable
}

@adrianVmariano
Copy link
Collaborator

Doc formatting looks ok to me---I don't see any obvious issues that will cause problems.

You have a sentence introducing limitations that are never articulated.

Is there a compelling reason to use three variables for the dimensions of the object instead of a vector parameter like cube() uses?

I am uncertain about whether roundingb should have its sign flipped. I understand why it works the way it does, but a value that is always supposed to be negative seems wrongly chosen, and in other contexts (e.g. join_prism) where a fillet is expected, the value is positive.

@amatulic
Copy link
Contributor

amatulic commented May 23, 2025

My feeling is that one parameter with three unnamed values would confuse me. I would prefer named parameters so I can remember which rounding is which.

base_w and base_h could be merged into a base_size vector, however.

For the Topics heading, I would include: Joiners, Parts

For the See Also heading, I would include: rounded_prism(), ycyl()

Remove the line "Some contraints to be aware of:" unless there are constraints to be aware of.

The documentation is automatically tested when you submit a PR, and you'll be notified if the tests fail.

Should this be part of joiners.scad, or a standalone file?

@coryrc
Copy link
Contributor

coryrc commented May 23, 2025

As still-a-newb, I would expect it to follow the convention of tube() for the rounded part and cube for the base; here's some code for it:

//   size = 3-vector of width and depth of baseplate, then height to center of ring cylinder (for half-circle, the flat portion)
//   id,ir = inner diameter or radius of the ring portion
//   od,or = minimum outer diameter or radius of the ring portion
module connector_ring(size=[10,5,20], ... ,od=undef,id=undef,or=undef,ir=undef,...) {
...
    size = scalar_vec3(size);
    base_w = size.x; thickness = size.y; base_h = size.z; // or do find/replace
    or = get_radius(r=or, d=od, dflt=5);
    ir = get_radius(r=ir, d=id, dflt=3)+get_slop();

I'm putting together a PR for a new joiner (a clamp with internal fins) so I've been going through this right now. There's some good ideas in here.

@dan-p3rry
Copy link
Author

Thanks for the feedback, guys.

You have a sentence introducing limitations that are never articulated.

Fixed.

I am uncertain about whether roundingb should have its sign flipped.

I am passing the roundingb variable directly to rounded_prism() without changing the sign.

Is there a compelling reason to use three variables for the dimensions of the object instead of a vector parameter like cube() uses?
base_w and base_h could be merged into a base_size vector, however.
size = 3-vector of width and depth of baseplate, then height to center of ring cylinder

I prefer Alex's suggestion. base_h is not the height of the prismoid, it's the z-location of the Y-cylinder. Z-height of the prismoid is the tangent point of prismoid & Y-cylinder.

My feeling is that one parameter with three unnamed values would confuse me. I would prefer named parameters so I can remember which rounding is which.

Please clarify exactly what change you are asking for.

or = get_radius(r=or, d=od, dflt=5);
ir = get_radius(r=ir, d=id, dflt=3)+get_slop();

I will implement this feedback.

@adrianVmariano
Copy link
Collaborator

I know that you passed the negative radius through directly. That's is in fact the justification for using a negative value there. But I think it should follow join_prism: rename roundingb to fillet and flip the sign when you pass through. That also helps distinguish the different rounding parameters.

I think roundingh needs to be either round_hole or hole_rounding. I checked for precedent in the library and there is round_top for a hinge, but really not much precedent.

Rename holeD_bool to D_hole.

Rename anchors hole_ctr_front and hole_ctr_back to just hole_front and hole_back. Note that CENTER is zero, so FRONT+CENTER is the same as FRONT. So also in the docs: "same as part BACK" not "BACK+CENTER".

I wonder if it would be better to refer to a "disk" instead of "Y-cylinder". You can just say "center" not "rotational center" of a cylinder (or disk). Also maybe write the description for someone who doesn't know what a prismoid is, so it's a self-contained explanation of what this module does for the person who only uses the parts library.

It seems like the first 4 parameters are pretty confusing because things that aren't the same are named the same and the order is surprising. I think either:

base = 2-vector giving x and y size of the base (e.g. like rect)
disk_center = height of the disk center
r = radius of disk

or

base_x = x dimension of base
base_y = y dimension of base
disk_center
r

I could also see doing length and thickness instead of base_x and base_y. It's crucial that r is the last of the positional parameters because d should be a named parameter alternative and then you won't give r, but you still must give the other 3.

And then you have ir, id for the hole. Should there be a parameter that is an alternative to ir/id that gives the ring width? Analogous to the wall parameter of tube()? In the code example for processing ir shown above, there were defaults. I prefer that we avoid arbitrary defaults in most cases.

I think that Alex was not asking for a change when he said he preferred named parameters. Rather, he was opposing my suggestion of using a vector of parameters. However, the expectation is that the positional parameters will be used without names, so the order should make sense and you shouldn't be relying on the names of those positional parameters to understand the named parameters.

Regarding the right file, I'm not sure. This doesn't seem like it belongs in joiners.scad. It doesn't function to join things. I had the idea of a new file called hooks.scad, but that would be a little more compelling if it already existed and was full of (other?) hooks. A "loop hook" does seem to be a concept out there. That could be a potential name for the module.

@amatulic
Copy link
Contributor

What Adrian said. Apologies if my earlier response was muddled.

Don't we already have a hook model somewhere? Or a snap clip? If we do, they could be merged into one file along with this. I was thinking of this D-ring as a sort of fastener to hang one object onto another; that is, join them. That's why I suggested joiners.scad, but maybe something like hooknring.scad (in the spirit of vnf.scad, which means "vertices 'n' faces") could work.

@dan-p3rry
Copy link
Author

I played around with the idea of size = [base_w, thickness, total_height], and tried it out in the code I'm writing that instigated this new module. This serve to remind myself that this part is all about placing a cylinder (screw, heat insert, whatever) a certain distance from a plane. So perhaps base_h should be hole_h (short version hh).
Agreed?

@dan-p3rry
Copy link
Author

dan-p3rry commented May 23, 2025

In the code example for processing ir shown above, there were defaults. I prefer that we avoid arbitrary defaults in most cases.

The default is 0, which tells the code to omit the hole. Do you prefer a separate Boolean? The rest of the feedback is complete or in progress.

Seems to me that adding the wall variable will require a holeBoolean...

@adrianVmariano
Copy link
Collaborator

The reason I remarked on the default is that in the example posted, the defaults were not zero.

It really seems like this object should be first and foremost a loop---that is, with a hole. In other words, it's not a thing with an optional hole. It's a thing with a hole...but you can turn the hole off. Given that nobody can think of a use case for a version with no hole, this seems like the right way to bill it. Maybe it should be required to specify a hole dimension somehow, and you can give zero for no hole. In the examples there is just one example at the very end that is "by the way, you can set ir to zero and get no hole".

If the above is the plan then I'd probably code that something like:

irtmp = get_radius(r=ir,d=id);
rtmp = get_radius(r=r,d=d);
dummy = assert(num_defined([irtmp,rtmp,wall])==2, "Must define exactly two of r/d, ir/id and wall");
ir = is_def(irtmp) ? irtmp 
                   : rtmp-wall;
r = is_def(rtmp) ? rtmp 
                 : irtmp+wall;
dummy2 = assert(ir<=r, "Hole doesn't fit or wall size is negative");

Or maybe you use or and od. Then if ir is zero you don't make a hole. I don't love wall as a parameter name, but it does match use of tube().

Regarding the vertical height parameter, hole_h is bad. What's the height of the hole? It's diameter? Distance to the top of the hole? Position to the hole center is very far down the list of what I think the "height" of a hole is. The parameter should be hole_center, or something along those lines. I had suggested disk_center above, but you're now acknowledging that the hole is important. I guess hole_z could work, though it's maybe less clear.

Some thought needs to go towards the right choice for the positional parameters. If the hole is primary for example, it should maybe be:

[base_x,base_y], hole_center, ir, wall

but that might be too many positional parameters. It might be that just base_x, base_y and hole_center should be positional.

@dan-p3rry
Copy link
Author

dan-p3rry commented May 26, 2025

I think should work as a final version.

// Module: connector_ring()
// Synopsis: Build a part generally used to anchor a cylinder or cylindrical hole to a flat surface
// SynTags: Geom
// Topics: Parts
// See Also: prismoid(), rounded_prism(), ycyl()
// Usage:
//   connector_ring(base_size, hole_z, or, [ir=], [D_hole=], [rounding=], [fillet=], [hole_rounding=], [anchor=], [spin=], [orient=])
// Description:
//   Form a part that would typically attach a cylinder or cylindrical hole parallel to, and a desired distance from a plane.  
//   Three rounding variables are used to customize the part.  
//   .
//   A couple of constraints to be aware of: In order to calculate a tangent where the base joins the cylinder, 
//   the lower corners of the base must be outside the cylinder (see Example 3).  This scenario occurs when
//   the base is narrower than the Y-cylinder and hole_z is less than Y-cylinder radius.  Also, hole_z must 
//   be large enough to accommodate hole rounding and base rounding. 
// Arguments:
//   base_size = vector, x- and y-dimensions of the base
//   hole_z = base height, z-dimension from the base to the center of the cylinder and optional hole
//   or = radius of the cylindrical portion of the part
//   ---
//   od = diameter of the cylindrical portion of the part
//   ir, id = optional radius/diameter of the center hole
//   D_hole = Boolean, set true to make the center hole to be semicircular.  False makes the hole to be circular.
//   rounding = rounding of the vertical-ish edges of the prismoid and the exposed edges of the cylinder
//   fillet = base rounding, set negative to form a rounded edge instead of fillet
//   hole_rounding = rounding of the optional hole
//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
//   spin = Rotate this many degrees around the Z axis.  See [spin](attachments.scad#subsection-spin).  Default: `0`
//   orient = Vector to rotate top towards.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
// Named anchors:
//   hole_front = front, center of the cylindrical portion of the part (same as the part FRONT if hole_z=or)
//   hole_back = back, center of the cylindrical portion of the part (same as the part BACK if hole_z=or)
//   tangent_right = right side anchor at the point where the prismoid merges with Y-cylinder, at y=0
//   tangent_left = left side anchor at the point where the prismoid merges with Y-cylinder, at y=0
// Example: Ring connector
//   connector_ring([50, 10], 25, 25, ir=20);
// Example: Widen the base, add base rounding, no hole
//   connector_ring([70, 10], 25, or=25, ir=0, fillet=3);
// Example: Narrow base, the corners of the base must be outside the Y-cylinder in order to calculate a tangent between the base and cylinder.
//   hole_z = 20;
//   base_size = [40, 10];
//   outer_radius = 25;
//   connector_ring(base_size, hole_z, outer_radius, ir=0);
//   up(hole_z) color("blue", 0.25) ycyl(r=outer_radius, h=base_size.y + 2);
//   right(0.5*base_size.x) color("red") ycyl(r=1, h=base_size.y + 2);
// Example: Through hole can be specified using or/od, ir/id, wall variables
//   ydistribute(spacing = 25) {
//     connector_ring([50, 10], 40, or=25, ir=20);
//     connector_ring([50, 10], 40, 25, wall=5);
//     connector_ring([50, 10], 40, wall=5, ir=20);
//     connector_ring([50, 10], 40, od=50, id=40);
//     connector_ring([50, 10], 40, od=50, wall=5);
//     connector_ring([50, 10], 40, wall=5, id=40);
//   }
// Example: Semi-circular through hole
//   connector_ring([50, 10], 12, 25, ir=15, D_hole=true, rounding=5, hole_rounding=5, fillet=5);
// Example: hole_z must be greater than 0 with no hole or D_hole=true.  hole_z must be greater than ir + hole_rounding + fillet when D_hole=false
//   connector_ring([50, 10], 1, 25, ir=0);
//   right(60) connector_ring([50, 10], 1, 25, ir=15, D_hole=true);
//   right(120) connector_ring([50, 10], 27, 25, ir=20, hole_rounding=3, fillet=3);
// Example: Rounding all edges
//   connector_ring([50, 10], 40, 25, ir=15, rounding=5, hole_rounding=5, fillet=5);
// Example: The connector_ring includes 4 custom anchors: front & back at the center of the cylinder component and left & right at the tangent points.
//   connector_ring([55, 10], 12, 25, ir=0) show_anchors();
// Example: Use the custom anchor to place a screw hole
//   include <BOSL2/screws.scad>
//   connector_ring([20, 10], 15, 7, ir=0, fillet=3) 
//      attach("hole_front") 
//        #screw_hole("M5", length=20, head="socket", atype="head", anchor=TOP, orient=UP);

module connector_ring(base_size, hole_z, or, ir, od, id, wall, D_hole=false,
            rounding=0, fillet=0, hole_rounding=0,
            anchor=BOTTOM, spin=0, orient=UP) {

    or_tmp = get_radius(r=or, d=od);
    ir_tmp = get_radius(r=ir, d=id);
    dummy = assert(num_defined([ir_tmp, or_tmp, wall])==2, 
                    "Must define exactly two of r/d, ir/id and wall");
    ir = is_def(ir_tmp) ? ir_tmp : or_tmp - wall;
    or = is_def(or_tmp) ? or_tmp : ir + wall;
    dummy2 = assert(ir <= or, 
                    "Hole doesn't fit or wall size is negative");
    assert(sqrt((0.5*base_size.x)^2 + hole_z^2) > or, 
                    "Base corners must be outside the cylinder");
    max_holeR = ir > 0 && fillet > 0 ? hole_z - fillet
                                : hole_z;
    if (ir > 0 && !D_hole) assert(ir + hole_rounding < max_holeR, 
                "ir + hole_rounding must be less than max_holeR");
    assert(hole_rounding >= 0, "hole_rounding must be greater than or equal to 0");

    z_offset = 0.5*(hole_z - or);
    tangents = circle_point_tangents(r=or, cp=[0, hole_z], 
                pt=[0.5*base_size.x, 0]);
    // we want the tangent with the larger y value
    tangent = tangents[0].y > tangents[1].y
            ? tangents[0] : tangents[1];
    // anchor calcs
    angle = atan((tangent.x - 0.5*base_size.x)/tangent.y);
    top_x = 0.5*base_size.x + (hole_z + or)*tan(angle);
    // when or > 0.5*base_size.x, need to move the anchor
    // use x^2 + y^2 = r^2, x = sqrt(r^2 - y^2)
    delta_y = z_offset;
    mid_x = sqrt(or^2 - delta_y^2);

    anchors = [
        named_anchor("hole_front", [0, -base_size.y/2, z_offset], FRONT, 0),
        named_anchor("hole_back", [0, base_size.y/2, z_offset], BACK, 0),
        named_anchor("tangent_right", [tangent[0], 0, tangent[1] - hole_z + z_offset], RIGHT, 0),
        named_anchor("tangent_left", [-tangent[0], 0, tangent[1] - hole_z + z_offset], LEFT, 0),
    ];
    override = [
        for (i = [-1, 1], j=[-1:1], k=[0:1])
            if (k==0 && j!=0 && or > 0.5*base_size.x)
                [[i, j, 0], 
                [mid_x*unit([i, 0, 0]) + 0.5*base_size.y*unit([0, j, 0])]]
            else if (k==0 && or > 0.5*base_size.x) 
                [[i, 0, 0], [mid_x*unit([i, 0, 0])]]
            else if (k==1 && j==0) 
                [[i, 0, 1], [or*sin(45)*unit([i, 0, 0]) 
                            + (z_offset + or*sin(45))*unit([0, 0, k])]]
            else if (k==1)
                [[i, j, 1], [or*sin(45)*unit([i, 0, 0]) 
                                + 0.5*base_size.y*unit([0, j, 0])
                                + (z_offset + or*sin(45))*unit([0, 0, k])]]
    ];

    attachable(anchor, spin, orient, 
                size=[base_size.x, base_size.y, hole_z + or],
                size2=[2*top_x, base_size.y],
                anchors=anchors, override=override) {
        up(z_offset) difference() {
            union() {
                top_half(s=2.1*max(base_size.x, 2*or, base_size.y), 
                            z=tangent.y - hole_z)
                    ycyl(r=or, h=base_size.y, rounding=rounding);
                up(tangent.y - hole_z)
                    rounded_prism(rect(base_size), 
                        rect([2*tangent.x, base_size.y]), h=tangent.y, 
                        joint_bot=-fillet, joint_sides=rounding, 
                        k_sides=0.92, k_bot=0.92, anchor=TOP);
            }
            if (ir > 0) {
                top_half(s=2.1*max(2*ir, base_size.y), 
                            z = D_hole ? 0 : -ir - hole_rounding - 1)
                    ycyl(r=ir, h=base_size.y + 0.01, 
                            rounding=-hole_rounding);
                if(hole_rounding > 0 && D_hole) {
                    rpath = path_merge_collinear( concat(
                        path2d(arc(cp=[ir, 0], r=hole_rounding, start=0, angle=-90)),
                        [[ir, -hole_rounding], [-ir, -hole_rounding], ],
                        path2d(arc(cp=[-ir, 0], r=hole_rounding, start=270, angle=-90)),
                    ));
                    for (m = [0, 1]) mirror([0, m, 0])
                        back(0.5*base_size.y) xrot(90) 
                        path_sweep(
                            mask2d_roundover(r=hole_rounding, 
                                    anchor=RIGHT), rpath);
                }
            }   // ir
        } // difference
        children();
    } // attachable
}

@amatulic
Copy link
Contributor

amatulic commented May 27, 2025

You still need a topics heading. I suggest "Parts" at a minimum. If I recall correctly, docsgen may fail without it.

The blank line before the first example will also cause a failure. Or it will cause none of the examples to process. Remove it.

There's some debate in the gitter chat about what file to put this in. We discussed making a new file for hooks and loops and putting it there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants