Works out a thread's diameters and profile from its size, picks the size that fits a round face, and builds the helical solid of a modeled thread.
How the precision is kept
Diameters and profile corners are 64-bit expressions on the nominal diameter and pitch from the size tables. The modeled thread is a polygon cross-section with steps of at most 360 ÷ segments degrees (96 by default) that manifold-3d extrudes with a twist, so the helical surface is a faceted approximation whose accuracy is set by the segment count. One diameter uses a factor rounded to six decimals.
Open item. Replace the rounded factor 0.649519 in the pitch diameter with 3 √3 ÷ 8.
Calculations under this policy
8 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The major, minor and pitch diameter and the thread depth of a size.
ISO: depth = 5√3 ÷ 16 × P, d₁ = d − 2 × depth, d₂ = d − 0.649519 P; trapezoidal: depth = P ÷ 2, d₂ = d − P ÷ 2
The depth uses the exact expression 5√3 ÷ 16. The pitch diameter uses the literal 0.649519, a six-decimal rounding of 3√3 ÷ 8 = 0.6495190528…, so d₂ differs from the exact value by about 5e-8 × P. In this file d₂ is used to match a bore to a size.
threads.js
lines 57 to 64
PartCraft 0.8.1
/** The basic profile's diameters: { d (major), d1 (minor), d2 (pitch), p, depth }. */
export function threadDims(form, size) {
const row = THREAD_TABLES[form]?.[size];
if (!row) return null;
const [d, p] = row;
const depth = form === 'tr' ? p / 2 : 5 * Math.sqrt(3) / 16 * p; // tr: H1 = P/2 · iso: 5H/8 with H = √3/2 · P
return { d, p, d1: d - 2 * depth, d2: form === 'tr' ? d - p / 2 : d - 0.649519 * p, depth };
}
Size that fits a round face
64-bit
Chooses the thread size nearest a shaft's or a bore's diameter.
shaft: nearest d; bore: nearest (d₁ + d₂) ÷ 2; accepted when the difference ≤ 0.12 × the face diameter
A plain search for the smallest absolute difference over the size table; the acceptance limit is relative to the face diameter (12 %).
threads.js
lines 66 to 79
PartCraft 0.8.1
/**
* The size that fits a round face of diameter `dia`: a shaft wants its major diameter (external), a bore its minor
* diameter (the tap drill: ≥ D1, below the pitch diameter). → the size key, or null when none is within 12 %.
*/
export function sizeFor(form, dia, external) {
let best = null, bd = Infinity;
for (const size of Object.keys(THREAD_TABLES[form] || {})) {
const g = threadDims(form, size);
const want = external ? g.d : (g.d1 + g.d2) / 2; // a bore: between the minor and pitch diameters
const dd = Math.abs(dia - want);
if (dd < bd) { bd = dd; best = size; }
}
return best !== null && bd <= 0.12 * dia ? best : null;
}
Profile corners over one pitch
64-bit
The crest, flank and root corners of the thread profile as axial position and radius.
The trapezoidal flat is computed from tan(π ÷ 12) rather than the rounded 0.366 P quoted in the comment. The clearance is added to both radii, so the profile is shifted, not scaled.
threads.js
lines 81 to 94
PartCraft 0.8.1
/**
* The profile over one pitch as (u, r) corners, u ∈ [0, P] along the axis, r the radius; crest first. `shift` moves it
* radially (− for a shaft's clearance, + for a bore's).
*/
export function profileCorners(form, size, shift = 0) {
const g = threadDims(form, size);
if (!g) return null;
const R = g.d / 2 + shift, r = g.d1 / 2 + shift, p = g.p;
// iso: crest P/8, flank 5P/16, root P/4, flank 5P/16 · tr: crest 0.366P, flank, root 0.366P, flank (flank = P/2·tan 15°)
const crest = form === 'tr' ? p * (0.5 - Math.tan(Math.PI / 12) / 2) : p / 8;
const root = form === 'tr' ? crest : p / 4;
const flank = (p - crest - root) / 2;
return [[0, R], [crest, R], [crest + flank, r], [crest + flank + root, r], [p, R]];
}
Profile radius at an axial position
64-bit
The radius of the thread profile at any position along the axis.
r = r₀ + (r₁ − r₀)(w − u₀) ÷ (u₁ − u₀), w = u mod P
Linear interpolation between corners after a positive modulo. The slack on the segment test and the guard on a zero-length segment are both 1e-15, absolute in millimeters.
threads.js
lines 96 to 105
PartCraft 0.8.1
/** The profile's radius at u (any real number; periodic in P). */
export function profileAt(corners, u) {
const p = corners[corners.length - 1][0];
const w = ((u % p) + p) % p;
for (let k = 1; k < corners.length; k++) {
const [u0, r0] = corners[k - 1], [u1, r1] = corners[k];
if (w <= u1 + 1e-15) return u1 - u0 < 1e-15 ? r1 : r0 + (r1 - r0) * (w - u0) / (u1 - u0);
}
return corners[0][1];
}
Cross-section of a modeled thread
64-bit
The polygon cut square to the axis whose twisted extrusion is the thread.
point = (r cos θ, r sin θ), r = profile(∓P θ ÷ 2π)
Angles are every profile corner plus steps of 2π ÷ max(24, segments); angles closer than 1e-9 rad are merged. Between vertices the outline is straight, so the section approximates the true curve to the segment count.
threads.js
lines 107 to 121
PartCraft 0.8.1
/**
* The thread's cross-section square to the axis at z = 0 as one CCW polygon [[x, y]…]: r(θ) = profile(u) with
* u = −P·θ/2π (right hand; left: +P·θ/2π), every profile corner a vertex, else steps of at most 360/segments degrees.
*/
export function boltSection(corners, { hand = 'right', segments = 96 } = {}) {
const p = corners[corners.length - 1][0];
const s = hand === 'left' ? 1 : -1;
const th = new Set([0, 2 * Math.PI]);
for (const [u] of corners) th.add((((s * u / p) % 1 + 1) % 1) * 2 * Math.PI); // θ where u hits a corner
const n = Math.max(24, segments);
for (let k = 1; k < n; k++) th.add(2 * Math.PI * k / n);
const list = [...th].sort((a, b) => a - b).filter((a, i, all) => i === 0 || a - all[i - 1] > 1e-9);
if (2 * Math.PI - list[list.length - 1] < 1e-9) list.pop();
return list.map((a) => { const r = profileAt(corners, s * p * a / (2 * Math.PI)); return [r * Math.cos(a), r * Math.sin(a)]; });
}
The extrusion with twist is done by manifold-3d. The code comment says a negative twist loses about 1e-3 of the volume in that library, so a left-hand thread is made by mirroring the right-hand solid instead.
threads.js
lines 123 to 137
PartCraft 0.8.1
/**
* The helical solid (a bolt's shape) from z = 0 to z = h along +z, twisted 360° per pitch; the screw motion keeps the
* profile phase at z = 0. M = manifold-3d, T = its arena. → a Manifold.
*/
export function boltSolid(M, T, corners, h, { hand = 'right', segments = 96 } = {}) {
const p = corners[corners.length - 1][0];
const poly = boltSection(corners, { hand: 'right', segments });
const cs = T(new M.CrossSection([poly], 'Positive'));
const turns = h / p;
const slices = Math.max(1, Math.ceil(turns * Math.max(24, segments)));
const right = T(cs.extrude(h, slices - 1, 360 * turns));
// a left-hand thread is the mirror image of a right-hand one (y → −y): manifold triangulates a negative twist's side
// quads across their fold, ~1e-3 of the volume, so it is never extruded that way
return hand === 'left' ? T(right.mirror([0, 1, 0])) : right;
}
Cutting tool of a modeled thread
64-bit
The solid that is cut from a shaft or a bore to leave the thread, with its clearance.
profile shifted by ∓clearance; start moved back by ceil(ext₀ ÷ P) × P
The tool starts a whole number of pitches early so the profile phase at the face does not depend on the extension. When a shaft's crest or a bore's root radius falls in the band of the faceted round face (from the facets' inner radius to the face radius, widened by 1e-4 × r each side), it is moved just outside that band, which changes that modeled radius by up to the facet sag plus 2e-4 × r.
threads.js
lines 139 to 167
PartCraft 0.8.1
/**
* A thread's cutting tool in its own frame (axis +z, the thread from z = 0 to z = len): a bore's = the bolt's shape
* (outward by `clearance`), a shaft's = a tube of radius `outer` less the bolt's shape (inward by `clearance`).
* ext0 / ext1: how far it carries on past z = 0 / z = len (past an open end, so the cut is clean). → Manifold
*/
export function threadTool(M, T, { form, size, external, len, clearance = 0, hand = 'right', segments = 96, outer = 0, ext0 = 0, ext1 = 0, face = null }) {
const corners = profileCorners(form, size, external ? -clearance : clearance);
if (!corners) return null;
// face { r, segments }: the faceted round face the thread is cut into. A shaft's crest (a bore's root) lying on those
// facets would leave hundreds of slivers of them, so it moves just inside the facets (a hair, at most their sag)
if (face) {
const lo = face.r * Math.cos(Math.PI / Math.max(8, face.segments)), eps = 1e-4 * face.r;
const k = external ? 0 : 2; // the corner pair on the face: crest (0, 1) or root (2, 3)
const v = corners[k][1];
if (v > lo - eps && v < face.r + eps) {
const to = external ? lo - eps : face.r + eps;
for (const c of corners) if (Math.abs(c[1] - v) < 1e-12) c[1] = to;
}
}
const h = len + ext0 + ext1;
// start the bolt one pitch early so the phase at z = 0 stays the same whatever ext0 is (a whole number of turns)
const p = corners[corners.length - 1][0];
const pre = Math.ceil(ext0 / p) * p;
const bolt = T(T(boltSolid(M, T, corners, len + pre + ext1, { hand, segments })).translate([0, 0, -pre]));
if (!external) return pre > ext0 + 1e-9 ? T(bolt.trimByPlane([0, 0, 1], -ext0)) : bolt;
const tube = T(T(T(M.CrossSection.circle(outer, Math.max(24, segments))).extrude(h)).translate([0, 0, -ext0]));
const cut = T(tube.subtract(bolt));
return cut;
}
Numbers in the thread callout
64-bit
Formats the pitch and the thread length written in a drawing's thread note.
text = value rounded to 3 decimals, trailing zeros dropped
toFixed(3) is applied to the value that goes into the callout text only; the feature keeps its unrounded length.
threads.js
line 179
PartCraft 0.8.1
const fmt = (v) => String(+Number(v).toFixed(3));
In detail
Method
Basic profiles as the file header states them: ISO 68-1 (60°, crest flat P ÷ 8, root flat P ÷ 4) and ISO 2901 (30° trapezoidal, depth P ÷ 2). The cross-section square to the axis is r(θ) = profile at u = ∓P θ ÷ 2π with every profile corner on a vertex; the solid is that section extruded with a twist of 360° per pitch by manifold-3d CrossSection.extrude.
Number types
64-bit numbers for dimensions, corners and section points. The solid is built inside manifold-3d; its number type is the library's and is not set in this file.
Tolerances
A size fits a face when the diameters differ by at most 12 % of the face diameter. Profile lookup slack 1e-15 (absolute). Section angles closer than 1e-9 rad are merged. A crest or root lying on the faceted round face is moved by 1e-4 × the face radius past the facets. A bore tool is trimmed when its early start exceeds the wanted extension by more than 1e-9.
Limits
The pitch diameter of an ISO thread uses 0.649519 in place of 3√3 ÷ 8. A left-hand thread is the mirror image of the right-hand solid. The callout text rounds pitch and length to three decimals.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.