cadquery-plugins
cadquery-plugins copied to clipboard
[Plugin Request] Thread Generator
Generating threads can often lead to invalid geometry. It would be nice to capture all of the best practices into a thread generator plugin.
For anyone looking into this, note that most users have trouble modelling threads using helical sweeps (which is the obvious way to model them in CadQuery). The main OCCT example (the bottle) models a thread using a different method, I think it would be worth looking into basing a plugin off that technique rather than helical sweeps. Seems that if it is OCCT's main example usage that it would be more robust.
PythonOCC has a demo that might help: https://github.com/tpaviot/pythonocc-demos/blob/master/examples/core_classic_occ_bottle.py
Actually there is an example of using this approach posted on the groups and it has been worked out further here: https://github.com/winksaville/cq-thread-experiments
Here are some experiences from Onshape users about a thread creator i made there:
Very popular features
- Internal/external threads, course and fine series
- Supported profiles: ansi standard, ansi squre, ansi acme, iso standard, square and DIN 103
- customize pitch
- Tapered start, with customized pitch and lead in
- left/right handed
- specify length or 'up to face'
- change depth of thread slightly for 3d printing tweaking
- automatically detect standard thread pitch based on a circle/diameter
Less popular features
- multi-start
- super-accurate/less accurate setting. This made a couple of optimizations to create threads that were 'good enough' but less computationally intensive than the correct profile-- ( specifically, the proper radius at the root vs just using straight lines which is fine for 3d printing)
Here's a copy of the docs for the Onshape thread creator:

If you're an onshape user, you can play with it here: https://cad.onshape.com/documents/6b640a407d78066bd5e41c7a/w/4693805578a72f40ebfb4ea3/e/f8aea9e5c33e02eab0854a4f
Here are some implementation details that might be useful: I ended up considering these thread classes:
export enum ASMEThreadClass{
_1A,
_2A,
_3A,
_1B,
_2B,
_3B
}
export enum ISOThreadClass{
_6e,
_6g,
_6h,
_6G,
_6H
}
These functions ( excuse odd Onshape javascript-like syntax) compute thread tolerances for the classes above:
export function calculateASMEThreadTolerances( pitch, majorDiameter, threadClass is ASMEThreadClass ){
//expanded words have units, letters are dimensionless for the ASME formulas
var threadHeight = 0.8660254 * pitch;
var pitchDiameter = majorDiameter - ( 3.0 * threadHeight / 4 );
var minorDiameter = majorDiameter - (5.0 * threadHeight / 4);
println("pitchDiameter=" ~ getDimensionlessValue(pitchDiameter,UOM.INCH) );
//short letters all are dimensionless
var P = getDimensionlessValue(pitch,UOM.INCH);
var D = getDimensionlessValue(majorDiameter,UOM.INCH);
var H = 0.8660254 * P;
var LE = 9 * P;
var n = (1 / P);
println("P=" ~ P ~ ",n=" ~ n ~ ",H=" ~ H ~ ",D=" ~ D ~ "LE=" ~ LE );
var internal = ( threadClass == ASMEThreadClass._1B || threadClass == ASMEThreadClass._2B || threadClass == ASMEThreadClass._3B );
var Td2 = 0.0015 * (D ^ (1/3)) + (0.0015 * sqrt(LE)) + (0.015 * P ^ ( 2/3 ));
println("Td2=" ~ Td2 );
if ( internal ){
var PT = undefined;
if ( threadClass == ASMEThreadClass._1B ){
PT = 1.95 * Td2;
}
else if ( threadClass == ASMEThreadClass._2B ){
PT = 1.3 * Td2;
}
else{ //3B
PT = Td2 * 0.975;
}
var maT = 0.14433757 * P;
var MAX_TOLERANCE = 0.394 * P;
var STD_TOLERANCE = (0.05 * P ^ ( 2/3 ) + 0.03 * P / D ) - 0.002;
var Td1 = STD_TOLERANCE;
if ( threadClass == ASMEThreadClass._3B ){
var MIN_TOLERANCE = undefined;
if ( n <= 12 ){
MIN_TOLERANCE = 0.12 * P;
}
else{
MIN_TOLERANCE = 0.23 * P - 1.5 * P ^ 2;
}
if ( STD_TOLERANCE > MAX_TOLERANCE ){
Td1 = MAX_TOLERANCE;
}
else if ( STD_TOLERANCE < MIN_TOLERANCE ){
Td1 = MIN_TOLERANCE;
}
else{
Td1 = STD_TOLERANCE;
}
}
else{ //1B and 2B
var MIN_TOLERANCE = 0.25 * P - 0.4 * P^2;
if ( D <= 0.25 ){
if ( STD_TOLERANCE > MAX_TOLERANCE ){
Td1 = MAX_TOLERANCE;
}
else if ( STD_TOLERANCE < MIN_TOLERANCE ){
Td1 = MIN_TOLERANCE;
}
else{
Td1 = STD_TOLERANCE;
}
}
else{
if ( n < 4 ){
Td1 = 0.15 * P;
}
else{
Td1 = 0.25 * P - 0.4 * P^2;
}
}
}
println("Td1=" ~ Td1 );
return {
'maxMinor' : (getDimensionlessValue(minorDiameter,UOM.INCH) + Td1)*inch,
'minMinor' : (getDimensionlessValue(minorDiameter,UOM.INCH) )*inch,
'minPitch' : (getDimensionlessValue(pitchDiameter,UOM.INCH) )*inch,
'maxPitch' : (getDimensionlessValue(pitchDiameter,UOM.INCH) + PT )*inch,
'maxMajor' : (getDimensionlessValue(majorDiameter,UOM.INCH)+ maT)*inch,
'minMajor' : (getDimensionlessValue(majorDiameter,UOM.INCH))*inch
};
}
else{ //external
var maT = undefined;
var PT = undefined;
var PA = undefined;
if ( threadClass == ASMEThreadClass._1A ){
maT = 0.090 * P ^ ( 2 /3 );
PT = 1.5 * Td2;
PA = 0.3 * Td2;
}
else if ( threadClass == ASMEThreadClass._2A ){
maT = 0.060* P ^ ( 2/3 );
PT = Td2;
PA = 0.3 * Td2;
}
else{ //3A
maT = 0.060* P ^ ( 2/3 );
PT = Td2 * 0.75;
PA = 0.0;
}
var miT = 0.216506 * P;
return {
'minMinor' : (getDimensionlessValue(minorDiameter,UOM.INCH) - PA - miT)*inch,
'maxMinor' : (getDimensionlessValue(minorDiameter,UOM.INCH) - PA)*inch,
'minPitch' : (getDimensionlessValue(pitchDiameter,UOM.INCH) - PA - PT)*inch,
'maxPitch' : (getDimensionlessValue(pitchDiameter,UOM.INCH) - PA)*inch,
'minMajor' : (getDimensionlessValue(majorDiameter,UOM.INCH) - PA - maT)*inch,
'maxMajor' : (getDimensionlessValue(majorDiameter,UOM.INCH) - PA)*inch,
};
}
}
export function calculateISOThreadTolerances(pitch, majorDiameter, threadClass is ISOThreadClass ){
//expanded words have units, letters are dimensionless for the ASME formulas
var threadHeight = 0.8660254 * pitch;
var pitchDiameter = majorDiameter - 3.0 * threadHeight / 4;
var minorDiameter = majorDiameter - 5.0 * threadHeight / 4;
var P = getDimensionlessValue(pitch, UOM.MM);
var D = getDimensionlessValue(majorDiameter, UOM.MM);
var H = 0.8660254 * P;
var LE = 9 / P;
var internal = ( threadClass == ISOThreadClass._6H || threadClass == ISOThreadClass._6G );
println("P=" ~ P ~ ",H=" ~ H ~ ",D=" ~ D ~ "LE=" ~ LE );
//ISO formulals all compute micrometers, so results are converted to MM
//Td -> major diameter Tolerance for bolt
//Td1 -> minor diameter Tolerance for nut
//Td2 -> pitch diameter tolerance for bolt
var NORMAL_DEVIATION = (15 + 11 * P) / 1000;
var Td = (180 * P ^ (2/3) - ( 3.15 / sqrt(P) )) / 1000;
var Td2 = (90 * P ^ 0.4 * D ^ 0.1) / 1000;
var Td1 = undefined;
if ( P < 0.8 ){
Td1 = (433 * P - 190 * P ^ 1.22) / 1000;
}
else{
Td1 = (230 * P ^ 0.7 ) / 1000;
}
println("Td=" ~ Td ~ " mm, Td1=" ~ Td1 ~ " mm, Td2=" ~ Td2 ~ " mm" );
if ( internal ){
var EI=undefined;
if ( threadClass == ISOThreadClass._6G ){
EI = NORMAL_DEVIATION;
}
else{ //6H
EI = 0.0;
}
var PT = Td2 * 1.32;
return {
'maxMinor' : (getDimensionlessValue(minorDiameter,UOM.MM)+EI + Td1)*millimeter,
'minMinor' : (getDimensionlessValue(minorDiameter,UOM.MM)+EI )*millimeter,
'minPitch' : (getDimensionlessValue(pitchDiameter,UOM.MM)+EI )*millimeter,
'maxPitch' : (getDimensionlessValue(pitchDiameter,UOM.MM) +EI + PT )*millimeter,
'maxMajor' : (getDimensionlessValue(majorDiameter,UOM.MM) +EI + Td1)*millimeter,
'minMajor' : (getDimensionlessValue(majorDiameter,UOM.MM)+EI )*millimeter
};
}
else{
var cmin = 0.35 * P; //approximated, actual rounding formula is a pain
var es = undefined;
if ( threadClass == ISOThreadClass._6g ){
es = NORMAL_DEVIATION;
}
else { //_6h
es = 0.0;
}
return {
'minMinor' : (getDimensionlessValue(minorDiameter,UOM.MM) - es - cmin)*millimeter,
'maxMinor' : (getDimensionlessValue(minorDiameter,UOM.MM) - es)*millimeter,
'minPitch' : (getDimensionlessValue(pitchDiameter,UOM.MM) - es - Td2)*millimeter,
'maxPitch' : (getDimensionlessValue(pitchDiameter,UOM.MM) - es)*millimeter,
'minMajor' : (getDimensionlessValue(majorDiameter,UOM.MM) - es - Td)*millimeter,
'maxMajor' : (getDimensionlessValue(majorDiameter,UOM.MM) - es)*millimeter,
};
}
}
These functions create the profiles ( again, Onshape syntax, but maybe helpful to save some work ):
function createButtressThreadProfile(sketch is Sketch, pitch, majorDiameter, internal,diameterAllowance, pitchDiameterAllowance ){
var P = pitch;
var baseLine = computeBaseLine ( majorDiameter, pitch, internal);
var BL = baseLine.BL;
var DIR = baseLine.DIR;
var f = 0.125 * P;
var d = 2/3*P;
var h = P - 2*f;
var p1 = vector(BL , f);
var p2 = vector(BL - d*DIR, h);
var p3 = vector(BL - d*DIR, h + f );
var p4 = vector(BL , h + f );
println("points=" ~ [p1, p2, p3, p4]);
skPolyline(sketch, "polyline1", {"points" : [ p1,p2,p3, p4, p1] });
}
function createSquareThreadProfile(sketch is Sketch, pitch, majorDiameter,internal,diameterAllowance, pitchDiameterAllowance)
{
var P = pitch;
var baseLine = computeBaseLine ( majorDiameter, pitch, internal);
var BL = baseLine.BL;
var DIR = baseLine.DIR;
//BL = BL + LITTLE_BIT;
var p1 = vector(BL, 0 * meter);
var p2 = vector(BL - (P / 2.0)*DIR, 0 * meter);
var p3 = vector(BL - (P / 2.0)*DIR, P / 2.0);
var p4 = vector(BL, P / 2.0);
println("points=" ~ [p1, p2, p3, p4]);
skPolyline(sketch, "polyline1", {"points" : [ p1,p2,p3, p4, p1] });
}
function createTrapezoidThreadProfile(sketch is Sketch, pitch, majorDiameter, threadAngle,internal,diameterAllowance, pitchDiameterAllowance)
{
var P = pitch;
var baseLine = computeBaseLine ( majorDiameter, pitch, internal);
var BL = baseLine.BL;
var DIR = baseLine.DIR;
var T = tan(threadAngle / 2.0);
var x = 0.3707 * P;
var s = (T * P / 2.0);
var p1 = vector(BL, 0 * meter);
var p2 = vector(BL - P / 2.0*DIR, T * P / 2.0);
var p3 = vector(BL - P / 2.0*DIR, s + x);
var p4 = vector(BL, x + (2.0 * s));
skPolyline(sketch, "polyline1", { "points" : [ p1,p2, p3,p4,p1 ] });
}
function createStandardThreadProfile(sketch is Sketch,internal,definition)
{
var pitch = definition.pitch;
var majorDiameter = definition.majorDiameter;
var minorDiameter = definition.minorDiameter;
var pitchAllowance = definition.pitchAllowance;
var superAccurate = definition.superAccurate;
var A_TINY_BIT=0.00001*meter;
var P = pitch;
var H = 0.8660254 * P;
var adjustedMajorDiameter = majorDiameter - pitchAllowance;
var adjustedMinorDiameter = minorDiameter - pitchAllowance;
var A=undefined;
var B=undefined;
var C=undefined;
var BL=undefined;
var DIR=undefined;
var TAN30 = tan(30*degree);
var P2 = P/2.0;
if ( internal ){
A = H/4.0;
B = 7*H/8.0;
C = 15*H/16.0;
BL = adjustedMinorDiameter /2.0;
DIR = 1.0;
}
else{
A = A_TINY_BIT;
B = 3*H/4.0;
C = 7*H/8.0;
BL = adjustedMajorDiameter /2.0;
DIR = -1.0;
}
var p1= vector(BL + DIR*A, -P2 + A*TAN30);
var p2 =vector(BL + DIR*B, -P2 + B*TAN30 );
var p3 =vector(BL + DIR*C,-P2 + C*TAN30 );
var p4 =vector(BL + DIR*C, 0*meter );
var p5 =vector(BL + DIR*C,P2 - C*TAN30);
var p6 =vector(BL + DIR*B,P2 - B*TAN30);
var p7=vector(BL + DIR*A , P2 - A*TAN30 );
if ( superAccurate ){
skLineSegment(sketch,"line1", { "start": p1, "end": p2 });
skArc(sketch, "arc1", { "start" : p2, "mid" : p4, "end" : p6 });
skLineSegment(sketch, "line2", { "start" : p6, "construction" : false, "end" : p7 });
skLineSegment(sketch, "line3", { "start" : p7, "construction" : false, "end" : p1 });
}
else{
skLineSegment(sketch,"line1", { "start": p1, "end": p3 });
skLineSegment(sketch,"line2", { "start": p3, "end": p5 });
skLineSegment(sketch, "line3", { "start" : p5, "end" : p7 });
skLineSegment(sketch, "line4", { "start" : p7, "end" : p1 });
}
}