dcowden / cadquery

CadQuery-- a parametric cad script framework
Other
432 stars 56 forks source link

Threads API and Tutorial? #195

Closed fragmuffin closed 5 years ago

fragmuffin commented 7 years ago

A tutorial on how to make a solid geometry thread would be very helpful.

I've (sort of) succeeded by using a few back doors in this gist (more information on implementation there) However, it takes a very long time to generate (~15sec), and it's not perfect.

Any advice on the best way to generate a thread? Perhaps make it into one of the example scripts.

fragmuffin commented 7 years ago

@dcowden

Your improvement list on the gist is good!... to repeat it:

It's funny to think that one of the oldest staples of mechanical engineering is so complex! But the fact is, a thread is a very complex structure; with the exception of the top and bottom face, there isn't a single flat, cylindrical, conical, or spherical face on a thread!

With regard to the cadquery direct API (#167) This highlights the need to:

I agree with your enhancements!, many will be quite simple to implement... the ones I'm more concerned with are:

Taper thread entry and exit The cross-section solution above doesn't translate easily to a helical sweep. In order to do this with a sweep, the cross section's scale would need to be altered throughout the sweep, something I don't see in the existing FreeCAD or OCC interfaces.

Although this article depicts a scale-varied thread (or impeller), but it's not clear how that was achieved.

Ball Screw It's not in your list, but still relevant. All thread profiles comprised of linear lines translate to a helical edge on the 2d profile, but a circular profile will be something else... my guess is an arc (which is actually simpler than a helix... hmm)

dcowden commented 7 years ago

Yes, sadly threads are complex. Have a look at this:

https://forum.onshape.com/discussion/4867/new-featurescript-thread-creator

I created something with most of these features for Onshape. It was hard, and even hard to get to perform well. A later version supported buttress threads as well ( which are really good for 3-d printing since they do not have overhangs)

I found in that work that a common use of arbitrary threadforms tend to be users prototyping medical devices. For that problem, there are a lot of odd threads.

I was also reminded looking back at that: i created a flag that allowed creating more or less accurate threads. The inaccurate versions creates 'fully sharp' threads. The even worse-performing version creates the actual correct fillets at the thread root and tip.

Ball screws are definitely in the list too. I ended up not creating them because they are highly dependent on the specific manufacturer ( IE, there didnt appear to be any applicable standards for their geometries )

jmwright commented 7 years ago

@fragmuffin As you and @dcowden have mentioned, threads are expensive computationally. However, I'd still be curious how the same thread created manually in FreeCAD (point-and-click GUI) does. I implemented the sweep operation quite awhile ago, but it's one of our newer operations and hasn't been used extensively yet, as far as I know. There could be some inefficiencies that have been introduced inside the CadQuery API.

If you want to include this thread generator in the official examples let me know. I'm in the process of cleaning up the examples, converting them to be compliant with the CadQuery Gateway Interface, and merging them all into the main library. Right now we've got examples in multiple places and you can't copy them between environments. After this update is done we'll be able to copy a script from the FreeCAD module into a Jupyter notebook and have it run without modifications.

dcowden commented 7 years ago

@jmwright @fragmuffin one other thing to note regarding threads.

In Onshape land, I implemented two features-- one is a thread creator, which creates arbitrary threads. The other is a hole/countersink creator, which allows you to pick BASED on standard thread sizes, but actually creates the necessary tap, clear, and csk holes.

Since Onshape tracks how many users reference each one, its a good way to see which ones are most popular. These two features are used about equally.

The interesting thing about the hole creator is that most of the code is actually python. Onshape uses weird lookup table constructs, so i actually forward generate their weird language using python. I solved the hard problem of computing the threaded length, socket dimensions, and all that other nasty stuff in python, so i can re-use that for CQ also.

fragmuffin commented 7 years ago

I found something a bit puzzling...

import cadquery as cq
from Helpers import show
import Part as FreeCADPart

pitch = 2
depth = 1
height = 10
radius = 2

thread = cq.Workplane("XY").newObject([
    cq.Shape.cast(FreeCADPart.makeThread(
        pitch, depth, height, radius
    ))
])

show(thread, (204, 204, 204, 0.3))

resulting thread object

there are so many things wrong with the result

So I don't know how it's supposed to be called, or how it can be used. :confused:

jmwright commented 7 years ago

@fragmuffin This code takes CadQuery out of the mix and gives the same result. I get the same behavior in both FreeCAD 0.16 and 0.17.

import Part

pitch = 2
depth = 1
height = 10
radius = 2

thread = Part.makeThread(pitch, depth, height, radius)

Part.show(thread)

screenshot_2017-09-19_09-19-28

When I check the height with the measure tool it's closer to 6mm than 10mm. I'm not sure what's going on yet, but at least we can eliminate CadQuery as the culprit.

jmwright commented 7 years ago

This threadmaker macro is interesting. They talk about high CPU utilization too.

https://www.freecadweb.org/wiki/Macro_screw_maker1_2

Search for "def cutIsoThread"

fragmuffin commented 7 years ago

@jmwright I checked out that plugin, it's very nice! But like everything else I've seen, those threads are made by sweeping a profile and cutting it from a cylinder. I couldn't shake the idea that threads could be made much more elegantly.

So I did it! I've been conversing with @tomate44 on the FreeCAD python forums about this (details in this thread, pun intended, but also unavoidable)

some examples ball_screw trapezoidal excavator

first working in this commit

@dcowden @jmwright do you think this has a place in the cadquery API? a function to convert a wire into a face so it may be swept into a thread?

jmwright commented 7 years ago

Nice work @fragmuffin ! Where do you think the wire-to-face operation would fit into the existing API?

fragmuffin commented 7 years ago

I don't know @jmwright

For the profile...

import cadquery
points = [(2, 0), (3, 1), (2, 2)]
profile = cadquery.Workplane("XZ").moveTo(*points[0]).polyline(points[1:]).wire()

Conversion Utility It's a conversion, rather than an operation...

"convert this wire into an equivalent cross-section face"

so perhaps it should be a utility on the side.

from cadquery import thread_profile_to_cross_section
cross_section = thread_profile_to_cross_section(profile)

Operation Or do you think it's an operation on a wire to turn it into a face, conceptually similar to a wire being extruded being "converted" into a solid.

cross_section = profile.threadProfile2CrossSection()
fragmuffin commented 7 years ago

Regarding our old friend:

Part.makeThread(pitch, depth, height, radius)

I found this in the OCC docs, the thread in the tutorial looks a lot like the one pictured in our previous posts.

https://www.opencascade.com/doc/occt-7.2.0/overview/html/occt__tutorial.html#sec4

dcowden commented 7 years ago

For what it is worth, when Parasolid is the kernel, i also found that it performed best when you revolve a profile along a helix, and then cut that from a cylinder. Making internal threads is much the same, you just flip your profile.

The nice thing about doing it that way is that you can make just about any thread profile using the same code-- the difference is the sketch you rotate.

fragmuffin commented 7 years ago

I feel like I'm so close!

I've built a fasteners basis with real threads so the following code: (using cqparts)

import cadquery
from Helpers import show

import cqparts
from cqparts.fasteners import Fastener
from cqparts.types import threads

screw_m3_4_5 = Fastener(
    head=('pan', {
            'diameter': 5.2,
            'height': 2,
            'fillet': 1,
        }
    ),
    drive=('phillips', {
            'diameter': 3,
            'depth': 2,
            'width': 0.6,
        }
    ),
    thread = (threads.iso_262.ISO262Thread, {
            'radius': 3.0 / 2,
            #'pitch': 0.35,  # build invalid shape
            'pitch': 0.7,
        }
    ),
    length=4.5,
)

show(screw_m3_4_5.object, (200, 200, 200, 0.3))

renders this m3x4 5

based on the above code, you can select from a number of heads, and a number of screw drives. heads screw_drives

However, threads are still very temperamental, and I can't see me solving it any time soon. the solutions in this forum discussion sometimes work, and sometimes don't. (and weirdly, sometimes they work when run from a FreeCAD console, and not from an ipython console on exactly the same object)

So I'm going to switch to a default of just rendering a cylinder instead of the real thread.

I still think accurate threads is a valuable asset to have, especially when it comes to 3d printable objects. but I think I need to put that on the back-burner.

If anybody feels like a challenge, check out https://github.com/fragmuffin/cqparts/issues/1 to see sample code to generate dud thread solids.

dcowden commented 7 years ago

Holy cow that's incredible! Amazing work!

Most users I've talked to end up being fine with the cylinder, even if threads are fast for one model they are too slow when you have many in an assembly. There seem to be two distinct user popuations. (A) users using standard thread sizes. They just want the cylinder, but if anything want to automatically drill the clearance or tap hole I'm the mating piece. They want normal tolerancing selections.(iso or ansi thread classes) (B) users designing custom threads, for example medical. These users do want the threads, and are probably going to 3d print it. They want fine grained control on the tolerances and generally want absolute tolerancing vs iso because they are going to adjust based in their particular printer. (For example, I want to generate a 0.75" x 40 tpi thread, oversized by 0.020" )

jmwright commented 7 years ago

@fragmuffin That's awsome. So the core issue is that if I tried to 3D print this screw right now it wouldn't be watertight, correct? Sounds like you and Dave have figured out a compromise. Could cylinder vs threads just be a parameter you pass to the generators?

For anyone wanting to try cqparts out, make sure that you grab the develop branch, open the Python console in FreeCAD, and enter the following, setting the correct path to where you've downloaded/cloned cqparts.

import sys
sys.path.insert(0, "[/path/to/download]/cqparts/src")
fragmuffin commented 7 years ago

@dcowden

(A) users using standard thread sizes. They just want the cylinder, but if anything want to automatically drill the clearance or tap hole I'm the mating piece

That's exactly what I'm designing in next... the user can create a fastener, then "apply" it to a work-piece. I'll let you know how that goes too ;)

(B) users designing custom threads, for example medical. These users do want the threads, and are probably going to 3d print it

well this is perfect for them too! this code defines a custom thread with any profile they want

import cadquery
from Helpers import show
from cqparts.params import *
from cqparts.types.threads import Thread, thread

@thread('my_thread')  # registration is not mandatory, but recommended
class MyThread(Thread):
    my_param = FloatRange(0, 5, 2.5)  # for example

    def build_profile(self):
        # can be any shape
        points = [
            (self.my_param, 0),
            (self.radius, self.pitch/2),
            (self.my_param, self.pitch),
        ]
        profile = cadquery.Workplane("XZ") \
            .moveTo(*points[0]).polyline(points[1:])
        return profile.wire()

screw_m3_4_5 = Fastener(
    # ... as above
    thread = ('my_thread', {
            'radius': 3.0 / 2,
            'pitch': 0.7,
            'my_param': 2.1,
        }
    ),
    length=5,
)
fragmuffin commented 7 years ago

@jmwright yes, you're right, not watertight :(

also, correct path can be universally set with this code. if you have folders side-by-side:

import os
import sys
import inspect

if 'MYSCRIPT_DIR' in os.environ:
    # the cadquery plugin adds `MYSCRIPT_DIR` to the environment
    _this_path = os.environ['MYSCRIPT_DIR']
else:
    _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
# or just adjust this relative path to suit your needs
sys.path.insert(0, os.path.join(_this_path, '..', 'cqparts', 'src'))

Could cylinder vs threads just be a parameter you pass to the generators?

yes, I'm going to do that too.. to only have real threads possible seems like a silly move, as @dcowden said, it's slow to build and render when there are a lot of them.

dcowden commented 5 years ago

This has been closed in favor of the CQ 2.0 implemention at https://github.com/cadquery/cadquery