Resources Numerical accuracy Calculation policy

Thread dimensions and modeled threads

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.

threads.js PartCraft 0.8.1 64-bit 8 calculations Reviewed

Tests tools/partcraft-test/test-screw-threads.mjs

Thread diameters

64-bit

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.

ISO: crest P ÷ 8, root P ÷ 4; trapezoidal: crest = root = P (0.5 − tan(π ÷ 12) ÷ 2); flank = (P − crest − root) ÷ 2

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)]; });
}

Helical solid

64-bit

The thread's solid shape from the cross-section.

turns = h ÷ P; slices = ceil(turns × max(24, segments)); twist = 360° × turns

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.

Every policy that takes this approach