Skip to content
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

Implementation of gradual bridging for holes #1467

Open
Andful opened this issue Aug 25, 2024 · 25 comments
Open

Implementation of gradual bridging for holes #1467

Andful opened this issue Aug 25, 2024 · 25 comments
Assignees

Comments

@Andful
Copy link

Andful commented Aug 25, 2024

Is your feature request related to a problem? Please describe.
When putting holes on bridged sections, it is common to gradually bridge around the hole. (In FDM printing)

Describe the solution you'd like
Make a module that implements the negative of the hole (without the cylindrical part).

Describe alternatives you've considered

  • Including the cylinder of the negative of the hole, but people might have strange hole shapes (cones, threaded, etc...)
  • Not include the rectangular base, but more often than not the rectangle is there. Maybe add a boolean option to exclude it?

Example Code

module bridged_hole(w, r, levels=3, layer_height=0.2) {
    assert (levels>=1)
    linear_extrude(layer_height) rect([w, 2*r]);
    for ( i = [2:1:levels]) {
        linear_extrude(layer_height*i) regular_ngon(pow(2,i), ir=r, spin=180/pow(2,i));
    }
}

Additional context

The result of the implementation:
bridged-hole

Complete code in the image.

#include <BOSL2/std.scad>

module bridged_hole(w, r, levels=3, layer_height=0.2) {
    assert (levels>=1)
    linear_extrude(layer_height) rect([w, 2*r]);
    for ( i = [2:1:levels]) {
        linear_extrude(layer_height*i) regular_ngon(pow(2,i), ir=r, spin=180/pow(2,i));
    }
}

difference() {
    cuboid([10, 10, 10], anchor=BOTTOM);
    bridged_hole(10, 2, 6, 0.2);
    cylinder(h=10, r=2, $fn=300);
}

A Fusion360 plugin that does the same:
https://github.com/Finn2708/CounterboreBridging

A Hackaday article about the process:
https://hackaday.com/2020/05/17/look-ma-no-support-for-my-floating-holes/

@adrianVmariano
Copy link
Collaborator

I found the example confusing. There's no hole being supported by the extra bridging. The normal application is a counterbore screw where you need to end the counterbore, so the hole needs to shrink.

It does seem like that idea would be a nice thing to add at some point.

@Andful
Copy link
Author

Andful commented Aug 30, 2024

I agree that the example is a bit odd. Ideally I would have included a sliced model, but did not have time for it. Would it still be useful to include that?

Indeed, the links I included are specifically for counterbore screw holes, but I personally experienced requiring this bridging procedure also for hexagonal nut pockets, or holes that happen to be in bridged section of my print. I personally would prefer keeping a module general enough for any hole. But specialized arguments may be added for the modules in screws.scad.

Let me know if any assistance is needed.

@adrianVmariano
Copy link
Collaborator

I don't know that a sliced model would help understanding the example. The thing that I don't understand is that the hole is not above a space and the only bridging I see is inside the hole. Hmm. Actually upon closer inspection I see that it's not inside the hole. It's hard to tell that.

I'm not sure about the right API for a general implementation. Do you pass it two (convex) polygons giving the shape of a larger hole underneath and smaller hole above? What would a general interface look like? You have a lot more bridges than I've seen anyone use for this. Is it necessary/useful? It means the bridging section gets thicker. Could be a user parameter, I suppose. It seems like figuring out the right API is perhaps the hardest part.

At the moment the dev focus is on bug fixes leading towards a stable 2.0 release, so we're trying to avoid being sidetracked by new features until after that release. But if you want to work on it, we can add the feature if you're able to get it to a reasonable state. Your preliminary code has z-fighting issues that you'd need to fix by adding extra amounts to the layers.

@adrianVmariano
Copy link
Collaborator

To be clear, I was thinking you pass it polygons as paths, not as geometry, so like bridged_hole(circle(r=6),circle(r=3),...)

It seems like you'd normally want the module to actually also make the (bottom) hole. Certainly if it's called "bridge_hole" it should do that. If that doesn't make sense it should be "hole_support" or something like that.

@Andful
Copy link
Author

Andful commented Aug 30, 2024

How would it work when fed an arbitrary shape? E.g. the smaller shape is a teardrop? I don't think the larger shape would matter too much, as long at the bridges span across the entire shape (but I might be wrong).

API seems indeed the hardest part...

@Andful
Copy link
Author

Andful commented Aug 30, 2024

Also the bottom bridges might not require to be rectangular. One might be able to fit more edges for better initial support.

E.G.
temp

The code in the image:

#include <BOSL2/std.scad>

$fn=600;

difference() {
    cuboid([100, 100, 1]);
    down(0.2) linear_extrude(1) circle(r=3);
    down(1) linear_extrude(100) circle(r=2);
    #down(0.4) linear_extrude(1) intersection() {
        regular_ngon(3, ir=2);
        circle(r=3);
    }
}

@Andful
Copy link
Author

Andful commented Aug 30, 2024

Seems like a hard problem in its most general form

@adrianVmariano
Copy link
Collaborator

Yes, I think the top hole shape is the one that matters. The outer hole matters only in that you have to make sure the bridges extend up to and probably slightly beyond that outer hole. There is polygon intersection code that I think should make that part easy. So one approach, limiting to orthogonal bridging, would be to pick a direction, examine the top shape and find its outermost point in that direction and create the two bridges that defines. Then go up a layer and do the orthogonal direction the same way. And then you can continue as may layers as needed. (How many layers are needed? Do you stop when the gap between the bridges and the hole is smaller than a threshold, which would presumably be an extrusion width?) I can see being able to implement this recursively where you pass a current "remaining hole" polygon, a bridge direction and the inner hole. Note that as long as the shape is convex, it doesn't matter what it looks like. There is the interesting notion of trying to force bridges to be parallel to segments of the hole, though. It seems like if you wanted a hexagonal hole you should be able to create that exactly with just three bridge pairs. But also it seems possibly better to use 2 layers of orthogonal bridging and then finish off with a layer featuring 4 "exact" bridges. I suppose this case could be detected because it would arise if you only had one segment left in the "hole" section.

A user option could permit starting with more bridges at the bottom, but you run the risk that they intersect each other if the top hole is not small enough compared to the bottom hole, so that case would need to be detected.

So yeah, being fully general requires some thought. Really nothing is fully general. So what requires thought is how to design an algorithm and API that is as general as possible without creating excessive complexity, but also without giving up to much if flexibility.

@amatulic
Copy link
Contributor

amatulic commented Dec 12, 2024

This is a category of problem called "unsupported holes", which is basically any vertical hole that has its bottom edge offset from the build plate when 3D printing. There are techniques to account for this, depending on what surrounds the bottom of the hole (like a round hole or a hex-shaped hole). I usually refer to this document on best practices for 3D printed designs for guidance.

It would be useful (and certainly would fill a need because I've had to do this many times in my own designs) to provide some cutouts for the most common use cases of unsupported holes, to accommodate a upside-down counterbores for bolts with a round head, square head, or hex head.

A few days ago I was considering revising screw_hole() in screws.scad to do exactly this, and then just now I discovered this open issue. The teardrop hole could also use some refinement, more like what's shown in the design guide I linked.

I would change screw_hole() to add new parameters:

  • head_sides = number of sides for the head (like 4 or 6), 0 or `undef' for round heads. I like designing things for hex bolts because then you don't need a wrench or screwdriver on the head; the 3D printed part holds it in place.
  • upside_down = true or false (default). If true, creates an upside-down counterbored screw hole with support structure.
  • layer_height = expected layer height for 3D printing (default 0.2), used for creating correct-thickness bridge offsets as well as better calibrated teardrop shaped horizontal holes.

I'm not keen on the gradual bridging proposal that requires six layers to complete, because slicers can use variable layer height, and the layer locations are not predictable several layers in. A transition with two or three layers can work, though.

@adrianVmariano
Copy link
Collaborator

I think if you want support structure it should be support=true. Not sure layer height should have a default. My default is 0.15. I'm not sure how to address the desire for hex shaped holes. The screw_hole module is complicated and uses the actual screw head. Square heads aren't supported---we don't have dimensions for them---so that wouldn't be possible...unless they were added to screw(). To create square/hex shaped countersinks there would probably need to be a new boolean parameter. I'm not sure what it should be called, that says to make the counterbore the shape of the head instead of round. I think this would only affect the head type "hex" right now, though.

Before unsupported holes can be added, a totally separate module that makes unsupported holes needs to be written. It looks like that document just shows a round hole. It should be possible to write something more generic that handles arbitrary convex shapes, as discussed above. I admit I wasn't clear on the merit of the approach with tons of layers.

Not sure everything in that document is true. Like horizontal axis threads are stronger than vertical ones due to layer lines. (You do have to omit part of the threads due to support.) I think we even implemented that in screw_hole.

Not sure what refinement teardrop needs. The point at the bottom? What's the point of that? I think changing teardrop's shape (e.g. adding that bottom point) has the potential to be hard. (I rewrote teardrop a while back and it was really annoying because of the low $fn handling.) That point seems kind of silly for most screw-hole scale holes, where $fn will be too small to really resolve it.

@amatulic
Copy link
Contributor

In my own library of code that I use outside of BOSL2, I already have some simple modules that make unsupported holes as well as horizontal holes. No rewrite is needed for teardrop because the shape isn't a teardrop, it's a special shape specifically for the purpose of a horizontal hole.

I've never needed to use square heads, but I have needed hex-shaped holes for recessed bolts. Square head specs are published, for example here: https://www.aftfasteners.com/square-head-bolts-mechanical-technical-information/

A default layer height of 0.2 would cover 0.15 layer height for about two layers, but this could be a required parameter instead of one with a default.

The refinement to the teardrop shape makes a difference for small horizontal holes, to avoid a flat surface at the bottom. The difference is significant because without it, I often need to drill the hole out a bit. For large holes, the little point at the bottom is always only 1 layer in height and not noticeable. The trapezoid at the top doesn't need to have 45° slopes; you can get away with 40° in all cases (and that's what the design guide I linked says), and the gap above needs to be at least 1 layer.

@adrianVmariano
Copy link
Collaborator

Well, actually teardrop IS a special shape for the purpose of horizontal holes. That's why Revar wrote it. Second sentence of the description reads: Useful for extruding into 3D printable holes. It is also used for 3d printable bottom roundovers. Should probably open a separate issue to discuss that, though, as it's got nothing to do with bridged holes.

Yeah, we have hex heads but not square heads. You can add square head screws if you like, but I still haven't recovered from working on the screws code and am not inclined to do it. You'd need to add a lookup table for the head thickness and sizes to _screw_info_english and code for actually creating square heads to screw_head. I would suggest the dimensions should be in fractions, rather than decimal. Don't know if square metric head info is easy to find or not. Generally the ideal way to find this stuff is in the right ASME or ISO document.

I wonder if we should introduce a $layer_height parameter of some kind that can be set/used by modules, similar to $slop.

The trouble with a default layer height is that you may forget the parameter exists, since you didn't specify it at first. If it's explicit you have more chance of seeing that you got it wrong if something changes. You'll never tell from looking at the model....

@amatulic
Copy link
Contributor

I know that's what the documentation says about teardrop, but in my experience that isn't the optimal shape for horizontal holes. It works for big holes but not for holes as small as 2mm, which I often use to make hinges with a piece of filament as the hinge pin. For holes that small you really need that little point at the bottom to ensure a rounded bottom.

Looking further, I see there are metric square nuts but metric square-head bolts don't exist, they're all inch units. The head sizes all fit the standard non-metric wrench sizes 3/8, 1/2, 9/16, 5/8, 9/16, 3/4, etc.

I agree layer height should be a required parameter, but it's a meaningless parameter if you're making a design for something other than FDM 3D printing. It may not matter at all for resin printing (I don't know). The documentation could say to set it to zero if it's irrelevant.

@adrianVmariano
Copy link
Collaborator

I haven't done much with tiny holes like that. I did do filament hinge pins once and recall having variability on how easy it was to insert the filament. @revarbat might have more thoughts on the matter, as he has much broader printing experience than I do. I admit that I'm a little puzzled about the problem. If it's that the realized hole doesn't circumscribe the ideal hole wouldn't increasing hole diameter be the answer? Or to put it another way, why is the slicer making a hole smaller than what I asked for?

But if it's the case that we sometimes need teardrops to have a point at the bottom, the answer is to elaborate teardrop to have a bottom-point option, not to introduce a second object that is exactly like teardrop except with a point.

@amatulic
Copy link
Contributor

amatulic commented Dec 13, 2024

Yes, it's preferable to improve the teardrop rather than introduce a new shape.

Many of my designs have fairly tight tolerances. Increasing the hole diameter beyond the normal clearance guideline of 0.3 clearance for a loose fit results in a rather sloppy and imprecise hinge that's too loose both vertically and horizontally. You can get away with a round-bottom hole if you use adaptive layer height, and then the slicer can adjust things so that the vertical position of each layer is exactly where it's supposed to be based on geometry. But with a fixed layer height, the integer multiples of layer thickness can result in holes being too tight vertically. At least that has been my experience. It's worse printing in draft mode with 0.3 mm layers.

I've written a first attempt at a generalized unsupported hole that works with hex, square, and round counterbores, as well as unsupported holes in the middle of a span between two walls. It's doing the bridging the same way for all counterbores (similar to the first example in this issue) instead of the trilateral way I do it for hex holes, but it looks OK.

There are a lot of parameters to this: bolt shaft diameter, head, width, head sides, shaft length, recess, layer height, and a boolean to tell the module to span between walls.
image
The hex hole would print better with a triangular arrangement of bridges, though.

@adrianVmariano
Copy link
Collaborator

I checked my design with the filament hinge pin and it appears I have $slop=0.05 which looks like it sets the hole to 0.1mm over the nominal filament diameter of 1.75mm. A clearance of 0.3mm seems enormous. I usually print 0.15mm layers and have never printed over 0.2mm. Revar's slop tester gives for me a slip fit with $slop=0.05 and a loose fit with $slop=0.1. I have a hard time remembering exactly how many times $slop gets worked into the model, though.

It sounds like you're thinking about this as a screw problem. Don't do that. It's a hole problem. My thinking was that you could have two paths, the bottom hole path and the top hole path. You can then work around the bottom hole, adding bridges and shrinking the remaining hole. The parameters are minimally: bottom path, top path, bottom height, and layer height. Is "span between walls" for making an unsupported rectangle with two opposing side walls missing? It's probably also desirable to include the total length and/or top hole length. (Probably both.)

This framework would handle all sorts of cases that might include off-center stacked holes, holes with irregular shapes, and so on. I guess the question is how hard it is to implement.

Can you elaborate on what goes wrong with more than 3 layers of support bridging?

@amatulic
Copy link
Contributor

amatulic commented Dec 13, 2024

There are a lot of clearance guidances published, recommending as much as 0.5 mm for a loose fit. There are going to be horizontal tolerance variations as you print, arising from:

  • Variations in the filament diameter. A typical spec you see on amazon is ±0.03 mm, which creates variations in volumetric flow, which in turn creates variations in line width.
  • Extrusion multiplier. This is filament-specific and most people don't calibrate it for each new spool they get. When I printed a cube with white silk PLA in vase mode with a line width of 0.45, I got a wall thickness of 0.5-0.54, which is considered an acceptable tolerance variation. But that means the perimeters from that filament are nearly 0.05 mm wider on each side. Generally slight overextrusion like this results in better quality parts.
  • Perimeter order. Usually one prints inner perimeters first, so that outer perimeters have something on the side to stick to when printing overhangs. However, because the inner perimeter may be squished a bit wider, the outer perimeter is going to get pushed slightly outward.

Printed vertical variations from the design arise mostly from:

  • Using constant increments of layer height. If your design has something 10.27 mm off the build plate, there is no way a 0.15 or 0.2 layer height can match it. I don't know the slicer algorithm, but I expect the slicer would add or omit a layer depending on which action results in the smallest error.
  • First layer. Typically the first layer is 0.2 mm unless you consciously set it to something else. The Prusa default printer profiles all have 0.2 mm for the first layer regardless of the layer height you print with. And that first layer, because it is squished onto the build plate, is not guaranteed to be 0.2 mm either.
  • In the case of horizontal holes, the edge of the perimeter, due to line width and printing outer perimeters last, may intrude a bit into the hole, and at the bottom of the hole this intrusion can result in an entire layer's worth of vertical clearance loss, which is why it's a good idea to put a 1-layer-deep point at the bottom of a teardrop.

These variations typically combine to cause 0.1 mm clearance horizontally to be a pretty tight fit. I find that 0.2 mm clearance for small holes is pretty snug. And the vertical variations are why the teardrop needs a little point at the bottom.

Which brings me to your question about what's wrong with 3 layers of bridging. More than 3 layers would likely work fine if the layer height in the design equals the layer height being printed. It may not be in exactly the right place due to the first layer thickness offset, but close enough to work. Where it goes awry is when you have the design layer height smaller than the print layer height, in which case you'll get the first layer of bridging and maybe the second, but likely not the third. To compensate for this, when I release a design to the public that has holes, I assume a layer height of 0.3 mm because printing at a smaller layer height is going to hit all the bridges, and may print two layers on one bridge, but that's OK.

@amatulic
Copy link
Contributor

I was thinking in terms of screws (or bolts) because the typical use case for unsupported holes is for counterbore holes with the counterbore on the underside. So I thought it would be best to give it parameters similar to screw_hole().

@adrianVmariano
Copy link
Collaborator

There's no reason the general implementation should be named after or designed based on a specialized application. For this to be used with screw holes, the screw() module I think will have to call it, so it doesn't matter what parameters the generic implementation uses for that application. Hmmm. Actually probably screw_head() has to call it. And so for a screw implementation, the length of "top" hole probably needs to be zero. (The top hole might be threaded.) The generic implementation should be considered in isolation to be the best designed generic implementation, with an API that makes sense generically. The original example above was a hole above a slot, so not a screw hole situation.

So it sounds like the 3 layer restriction is not real. It's an artifact of wanting to make designs that are more easily portable across mismatched layer thicknesses. What that tells me is that an implementation of bridged holes should not adhere to a 3 layer restriction. That is, if using more layers can produce a better result in some case, it should be possible to produce the better result with more layers. But a layer max or layers-desired setting should allow the user to say use at most (exactly) 3 layers of bridging.

I recall seeing bridges in prusaslicer that were two layers thick. Is that the norm? That would suggest that you might need 0.3mm layer thickness in the design when running at 0.15mm layer thickness.

@amatulic
Copy link
Contributor

Those points are sensible. It should be a general solution to the problem unrelated to screws, and there is no need for a 3-layer restriction, because if it fails to print properly after 2 or 3 layers, it won't really matter to the printed result.

I don't know of any norm for bridge thickness. A bridge is a line of plastic that has to bridge between two points. Whatever is printed on top of that isn't identified as a bridge in the slicer, because it has a surface underneath. PrusaSlicer does have a setting for printing thicker bridges, but that's thicker for only the bridging layer.

@amatulic
Copy link
Contributor

amatulic commented Dec 13, 2024

I think I have something that tries to find the shortest bridges, but it requires the unsupported top hole to be convex. The bigger bottom hole can be any shape as long as it has at least four sides. Would those restriction be OK? I suppose a non-convex top hole could always have hull() applied to it inside the module for the analysis.

polygon_line_intersection() is useful here, but is there some easy way to determine the polygon indexes around the intersection points?

@adrianVmariano
Copy link
Collaborator

That sounds ideal. The top hole has to be convex for this method to even make sense, no? A bridge needs to touch two sides of the bottom hole. It also needs to touch the top hole, or it's not helping. That's impossible for concave points on the top polygon. So the only way this would work is if the concave regions are small enough that you can work on the hull of the top and ignore the concavities. I say let the user hull the input if they want this kind of thing.

It seems like another requirement is possibly that the top hole polygon is contained within the bottom hole polygon, though the solution does still exist if this fails. In the extreme case the polygons are disjoint and you don't have any bridges at all. I'm also not sure about concave bottom hole. It seems like it may create some complications without adding much utility, so I wouldn't object to a convexity requirement there either.

I think you'll need to use the internal function _region_region_intersections() to get the polygon segment indices. Be sure to set closed=false for the "region" which is the line.

@amatulic
Copy link
Contributor

amatulic commented Dec 13, 2024

Well, my basic test case is a glued_circle() as the bottom hole. I think it is possible, but if it turns out to have weird results I can restrict it to convex.

Is there a built-in function that tests if a point is on a line between two other points? It's easy enough to write but I'd rather not duplicate it if it exists. The reason I ask, is that polygon_line_intersection() already returns the intersection points, so all I'd need to do is test whether an intersection point is between any two adjacent points on the polygon to find the indices. That seems more efficient than all the stuff _regions_region_intersection seems to be doing, and the situation here is pretty simple.

Edit: Looking further, it seems _regions_region_intersection isn't doing any math operation slower than norm(). I'll try that out first.

Edit 2: Yes, I can work with _regions_region_intersection(). It seems to return the lower index point of the line segment that is intersected.

@adrianVmariano
Copy link
Collaborator

Does performance seem to be a concern? It looks like the only extra stuff going on is the processing at the end which looks for repeated points, which might be significant if there are lots of points.

Test if a point is on a line segment given by two other points: is_point_on_line().

You can run a list comprehension with line_intersection using the polygon segments and construct a list of the intersection segment indices and points. We did try to optimize all of this code though so I don't know if you'd get a big speed improvement. I guess there's the question of whether you're using a convex shape where weird stuff like self-touching is impossible and doesn't need to be checked for.

Note that polygon_line_intersection is heavier than _region_region_intersection(). It calls split_region_at_region_crossings which in turn calls _region_region_intersections AND does additional post-processing.

@amatulic
Copy link
Contributor

Just an update. I did come up with an algorithm, but I have set it aside for a while because it was giving me a headache. Too much instability for specific cases, partly arising from me not fully understanding how _region_region_intersections() should work when segments are collinear, as well as difficulty constructing new polygons without resorting to a slow intersection operation.

I did decide that the optimal approach is likely not to greedily find the shortest bridges, but find the bridges that lop off the most area from the big hole.

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

4 participants