ThreadFeatures
OCCTSwift’s thread feature API lives in Sources/OCCTSwift/ThreadFeatures.swift. It adds three Shape methods (threadedShaft, threadedHole, threadedRod) plus the supporting value types ThreadSpec, ThreadProfile, ThreadForm, ThreadBuild, and RunoutStyle. OCCT ships no kernel “thread feature”; all thread geometry is composed from already-wrapped OCCT primitives (Shape.loft, Wire.arc, Wire.interpolate, Shape.sew) in Swift, the bridge is invoked only for the fallback smooth-helicoid cutter (OCCTShapeBuildThreadCutter).
Topics
Threaded features on Shape
Shape.threadedShaft(axisOrigin:axisDirection:spec:length:starts:runout:build:)
Cuts a helical V-profile external thread into a cylindrical shaft.
public func threadedShaft(axisOrigin: SIMD3<Double>,
axisDirection: SIMD3<Double>,
spec: ThreadSpec,
length: Double? = nil,
starts: Int = 1,
runout: RunoutStyle = .none,
build: ThreadBuild = .auto) -> Shape?
When self is a plain cylinder coaxial with the axis (the common case), this builds the threaded rod directly with no boolean for every build mode (.boolean, deprecated in #254, was removed as a case in v2.0.0): the thread’s true cross-section (a “cam”: root arc → flank → crest arc → flank) is lofted at closely-spaced z-slices rotated by the helix (ruled=false), giving a smooth, BRepCheck-valid solid of a handful of B-spline faces. Multi-start threads (starts > 1) build directly too (#257): N teeth tile the turn at lead = N·pitch, sampled per pitch so the loft stays in-envelope, giving a continuous interleaved multi-helix. Any unthreaded margin is closed by pure sewing (per-start shoulder + cylinder + end disk). Because the boolean engine is never invoked, the result is orientation-robust and valid where a cut-the-cutter approach is faceted or fails. For non-cylinder targets, rounded/tapered forms, or when the direct build fails, the method falls back to the boolean cut path (applyThreadCut).
- Parameters:
axisOrigin: a point on the shaft axis (typically the centre of the bottom face).axisDirection: unit vector along the shaft axis (normalised internally).spec: thread form and dimensions.length: threaded length in mm (default:2 * spec.nominalDiameter).starts: number of thread starts (1 for standard fasteners; >1 for lead screws). Multi-start forces the boolean cut path.runout: thread termination style at each end (seeRunoutStyle).build: construction path selector;.auto(default) and.directuse the smooth direct build for single-start coaxial cylinders..booleanwas removed as a case in v2.0.0, having been deprecated in #254; a legacy"boolean"JSON key still decodes, resolving to.direct. SeeThreadBuild.
- Returns: Threaded shape, or
nilon sweep / boolean failure. UseboundingBoxOptimal()(notbounds) to measure the true crest radius, the BSpline pole hull overshoots by ~13–21%. - OCCT: Pure-Swift direct path:
Shape.loft,Wire.arc,Wire.interpolate,Wire.join,Shape.face(from:),Shape.sew,Shape.solidFromShell, no boolean. Fallback cut path:OCCTShapeBuildThreadCutter(bridge: analytic helicoid cutter),Shape.screwSweptThreadCutter,Shape.subtracting. - Example:
guard let shank = Shape.cylinder(radius: 6, height: 24) else { return } let spec = ThreadSpec(form: .iso68, nominalDiameter: 12, pitch: 1.75) guard let threaded = shank.threadedShaft(axisOrigin: .zero, axisDirection: SIMD3(0, 0, 1), spec: spec, length: 18) else { return } // threaded.isValid == true; ~9 faces (smooth), crest at nominal radius (6 mm) - Note: Multi-start threads (
starts: 2,3, …) build via the smooth direct path too (#257). The tapered pipe forms (.nptTapered,.bsptTapered) and rounded forms (.knuckle, rounded Whitworth) always use the cut path since the smooth direct build supports parallel piecewise-linear forms only.
Shape.threadedHole(axisOrigin:axisDirection:spec:depth:starts:runout:)
Cuts a helical V-profile internal thread into an existing bore.
public func threadedHole(axisOrigin: SIMD3<Double>,
axisDirection: SIMD3<Double>,
spec: ThreadSpec,
depth: Double? = nil,
starts: Int = 1,
runout: RunoutStyle = .none) -> Shape?
Always uses the boolean cut path (applyThreadCut with apexSign: +1): a helical cutter is swept into the bore wall and subtracted from self. Because the cutter is cut into a thick wall (not a thin shaft), OCCT’s boolean handles a smooth (ruled=false) helical cutter robustly, so internal threads come out smooth and BRepCheck-valid. self must already contain the bore (a cylinder subtracted out, or any through-hole body).
- Parameters:
axisOrigin: point on the bore axis (typically the centre of the entry face).axisDirection: unit vector along the bore, pointing into the solid material.spec: thread specification.depth: axial length of the threaded region in mm (default:2 * spec.nominalDiameter).starts: number of thread starts.runout: thread termination style.
- Returns: Shape with the tapped thread cut, or
nilon boolean failure. - OCCT:
OCCTShapeBuildThreadCutter(analytic helicoid bridge, ISO-68/Unified only),Shape.screwSweptThreadCutter(fallback),Shape.subtracting. - Example:
let spec = ThreadSpec(form: .iso68, nominalDiameter: 12, pitch: 1.75) guard let outer = Shape.cylinder(radius: 12, height: 16), let bore = Shape.cylinder(radius: spec.minorDiameter / 2, height: 16), let block = outer.subtracting(bore) else { return } let tapped = block.threadedHole( axisOrigin: .zero, axisDirection: SIMD3(0, 0, 1), spec: spec, depth: 14) // tapped?.isValid == true - Note: Pass the solid body with the bore already in it,
threadedHoledoes not drill the hole. The outer surface and outer diameter are unchanged; only the bore wall gains the helical thread form. Pre-bore to the spec’sminorDiameter / 2(the tap-drill size): a correctly-cut internal thread’s crest sits there, untouched, and its root reaches out to the major/nominal diameter, matching a mating bolt’s own crest and root (#1578).
Shape.threadedRod(customProfile:nominalDiameter:pitch:cutDepth:length:axisOrigin:axisDirection:leftHanded:)
Builds a smooth threaded rod from a custom radial cross-section, directly and with no boolean.
public static func threadedRod(customProfile: ThreadProfile,
nominalDiameter: Double,
pitch: Double,
cutDepth: Double,
length: Double,
axisOrigin: SIMD3<Double> = .zero,
axisDirection: SIMD3<Double> = SIMD3(0, 0, 1),
leftHanded: Bool = false) -> Shape?
The entry point for threading a cylinder with a custom tooth shape (worm, screw conveyor, proprietary fastener). Composes the thread region (a ruled=false cam-slice loft of the profile swept along the exact helix) with the core cylinder by pure sewing, no boolean is invoked, so the result is BRepCheck-valid and analytic (a small number of B-spline faces, not a faceted multi-MB solid). The cross-section is a ThreadProfile in normalised (axial, depth) coordinates: axial 0…1 spans one pitch, depth 0 = crest (at nominalDiameter / 2) … 1 = root (at nominalDiameter / 2 − cutDepth). The profile must satisfy ThreadProfile.supportsSmoothRodBuild (a real crest flat, ≤ 2 flank segments); rounded or many-flank profiles do not satisfy this and return nil. For standard named forms (ISO, Unified, ACME, …), prefer threadedShaft with a ThreadForm spec.
- Parameters:
customProfile: normalised tooth cross-section; must satisfysupportsSmoothRodBuild.nominalDiameter: outer (crest) diameter in mm.pitch: axial advance per turn in mm.cutDepth: radial depth crest → root in mm (must be <nominalDiameter / 2).length: threaded length along the axis in mm.axisOrigin: a point on the rod axis at the thread start (default.zero).axisDirection: rod axis direction (defaultSIMD3(0, 0, 1)).leftHanded: helix handedness (defaultfalse= right-hand).
- Returns: A valid, smooth threaded rod, or
nilif inputs are degenerate, the profile does not satisfysupportsSmoothRodBuild, or the direct build cannot produce a valid solid. Does not silently fall back to a boolean result. - OCCT: Pure-Swift composition:
Shape.loft,Wire.arc,Wire.interpolate,Wire.join,Shape.face(from:),Shape.sew,Shape.solidFromShell, no boolean, no bridge cutter. - Example:
guard let tooth = ThreadProfile(vertices: [ .init(axial: 0.000, depth: 1), .init(axial: 0.125, depth: 1), .init(axial: 0.375, depth: 0), .init(axial: 0.625, depth: 0), .init(axial: 0.875, depth: 1), .init(axial: 1.000, depth: 1), ]), let worm = Shape.threadedRod(customProfile: tooth, nominalDiameter: 12, pitch: 5, cutDepth: 1.8, length: 22) else { return } // worm.isValidSolid == true, smooth, analytic (handful of B-spline faces) - Note:
ThreadProfile.supportsSmoothRodBuildrequires a real crest flat (hasCrestFlat == true) and at most two flank segments. Pointed-crest or multi-segment rounded profiles (knuckle, custom sinusoidal) returnnilhere and must usethreadedShaftwith the boolean cut path instead.
Threaded features: internal build strategy (not public API)
threadedShaft, threadedHole and threadedRod above are the whole public surface; the four functions on this page are private/fileprivate implementation behind them, documented here because Shape is the package’s most-read type and this is the mechanism ThreadBuild.auto/ .direct (see ThreadBuild below) actually selects between.
OCCT ships no “thread feature” primitive, so this package builds one two ways, tried in order:
- Direct build (#213), no boolean. When the target is a plain cylinder coaxial with the axis (the common case for an external thread), the thread’s true cross-section, a “cam”: root arc, flank spiral, crest arc, flank spiral, is lofted (
ruled=false) at z-slices rotated by the helix, giving a handful of B-spline faces rather than hundreds of facets. Any unthreaded margin is closed by pure sewing (shoulder face + plain cylinder + end disk). Because the boolean engine is never invoked, the result is orientation-robust andBRepCheck-valid where the cut path below is faceted or fails outright. This also covers multi-start threads (#257):Nteeth tile the turn at lead= N * pitch. - Boolean cut fallback. For non-cylinder targets, internal threads (
threadedHolealways uses this path), rounded or tapered forms, or whenever the direct build above declines: a V-groove cutter is swept along the helix and subtracted from the blank. The groove is sized from the profile’s own flats, its bottom the root-flat width and its mouth the inter-crest span, read throughThreadProfile.flatWidthFraction(atDepth:)rather than re-derived at the cutter (#991). The cutter itself has two variants (seescrewSweptThreadCutterbelow); a sound cut is verified geometrically (stays within the blank’s optimal bounding box, removes some but not most of the volume) rather than by trustingBRepCheck, since a long faceted thread can trip a benign facet self-intersection while still being dimensionally correct and STEP-exportable (#193).
Both paths clock the thread from the same angular datum: the perpendicular to axisDirection that OCCT’s own gp_Ax2(gp_Pnt, gp_Dir) picks as its Y direction, obtained from the module-wide perpendicularBasis(to:) that Placement, ConstructionPlane and Drawing already share (#990). The clocking is a function of axisDirection alone, so the same axis always yields the same start angle, but two axes that differ only in sign do not: +Z and -Z are half a turn apart. No API here specifies a start angle, since a thread rotated about its own axis is the same thread; what this does guarantee is that a shaft and the hole it screws into, built on the same axis, are clocked alike. Before #990 the datum came from a separate cross(axis, worldUp) construction local to ThreadFeatures.swift; it agreed with the shared one on +Z, +X and +Y, so threads on those axes are unchanged, and was half a turn away on -Z, -X and -Y.
ThreadSpec
Full specification of a thread’s form, diameter, pitch, and handedness. Sendable, Hashable, Codable.
public struct ThreadSpec: Sendable, Hashable, Codable
The six stored properties below are set once, at construction, by whichever init was used; every other member on this page (profile, cutDepth, taperRatio, …) is a computed property derived from them.
| Property | Type | Meaning |
|---|---|---|
form | ThreadForm | Which standard thread geometry this spec describes. |
nominalDiameter | Double | Outer (crest) diameter in mm. |
pitch | Double | Axial advance per revolution in mm. |
leftHanded | Bool | true for a left-hand helix. |
customProfile | ThreadProfile? | Cross-section for form == .custom; ignored for every other form. Set via the custom initializer. |
customCutDepth | Double? | Overrides the form’s default radial depth (mm). Required for .custom; optional elsewhere. |
ThreadSpec.customCutDepth
ThreadSpec.init(form:nominalDiameter:pitch:leftHanded:customProfile:customCutDepth:)
General-purpose initialiser.
public init(form: ThreadForm, nominalDiameter: Double, pitch: Double, leftHanded: Bool = false,
customProfile: ThreadProfile? = nil, customCutDepth: Double? = nil)
- Parameters:
form: thread form (seeThreadForm).nominalDiameter: outer (crest) diameter in mm.pitch: axial advance per revolution in mm.leftHanded:truefor left-hand helix (defaultfalse).customProfile: tooth cross-section forform == .custom; ignored otherwise.customCutDepth: overrides the form’s default radial depth (mm); required for.custom.
ThreadSpec.init(customProfile:nominalDiameter:pitch:cutDepth:leftHanded:)
Convenience initialiser for a fully custom tooth shape, sets form to .custom and stores customProfile/cutDepth.
public init(customProfile: ThreadProfile, nominalDiameter: Double, pitch: Double,
cutDepth: Double, leftHanded: Bool = false)
- Parameters: as above;
cutDepthbecomescustomCutDepth.
ThreadSpec.parse(_:)
Parses a standard thread designation string.
public static func parse(_ text: String) -> ThreadSpec?
Recognises metric M5x0.8 / M10 (coarse-pitch table); Unified / UNC / UNF 1/4-20 UNC, 3/8-16; trapezoidal Tr40x7 / Tr40x7LH; ACME 1.5-4 ACME; Whitworth W1/2 / 1/2 BSW; BSP parallel G1/2; BSP taper R1/2 / Rc1/2; NPT 1/2-14 NPT. Input is trimmed of whitespace before matching.
- Parameters:
text, designation string. - Returns:
ThreadSpec, ornilon unrecognised input. - OCCT: Pure-Swift, no bridge calls.
- Example:
ThreadSpec.parse("M5x0.8") // .iso68, Ø5, pitch 0.8 ThreadSpec.parse("M10") // .iso68, Ø10, pitch 1.5 (coarse table) ThreadSpec.parse("1/4-20 UNC") // .unified, Ø6.35, pitch 1.27 ThreadSpec.parse("Tr40x7LH") // .trapezoidal, Ø40, pitch 7, leftHanded ThreadSpec.parse("G1/2") // .bspParallel, Ø20.955, 14 TPI ThreadSpec.parse("1/2-14 NPT") // .nptTapered
ThreadSpec.profile
The tooth cross-section for this spec’s form, or the custom profile.
public var profile: ThreadProfile { get }
Dispatches on form: ISO-68 / Unified / NPT → ThreadProfile.iso60V(); Whitworth / BSP / BSPT → .whitworth55; ACME → .acme29; trapezoidal → .trapezoidalMetric30; square → .square; buttress → .buttress; knuckle → .knuckle; custom → customProfile ?? .iso60V().
- Returns:
ThreadProfileinstance for use by the modeller. - OCCT: Pure-Swift.
ThreadSpec.cutDepth
Practical radial thread depth (crest → root), form-dependent.
public var cutDepth: Double { get }
customCutDepth overrides when set. Standard values: ISO-68 / Unified / NPT = 5H/8; Whitworth / BSP / BSPT = 0.640327 × pitch; ACME / trapezoidal / square = 0.5 × pitch; knuckle (DIN 405) = 0.55 × pitch; buttress (DIN 513) = 0.86777 × pitch.
- Returns: Radial depth in mm.
- OCCT: Pure-Swift.
ThreadSpec.taperRatio
Diametral taper rate (NPT / BSPT are 1:16; all parallel forms are 0). The radius changes by taperRatio / 2 per unit of axial length.
public var taperRatio: Double { get }
- Returns:
1.0 / 16for.nptTaperedand.bsptTapered;0otherwise. - OCCT: Pure-Swift.
ThreadSpec.halfFlankAngle
Half of the 60° included angle (ISO-68 / Unified).
public var halfFlankAngle: Double { get }
Returns Double.pi / 6 (30°). Meaningful only for ISO-68 and Unified forms; use spec.profile for other forms.
- Returns:
π/6. - OCCT: Pure-Swift.
ThreadSpec.theoreticalDepth
Theoretical (untruncated) 60° V thread depth, H = pitch × √3 / 2 per ISO-68.
public var theoreticalDepth: Double { get }
- Returns:
pitch * sqrt(3) / 2. - OCCT: Pure-Swift.
ThreadSpec.crestFlat
Axial width of the truncated crest flat (ISO-68 external). Equal to P/8.
public var crestFlat: Double { get }
- Returns:
pitch / 8. - OCCT: Pure-Swift.
ThreadSpec.rootFlat
Axial width of the truncated root flat (ISO-68 external). Equal to P/4.
public var rootFlat: Double { get }
- Returns:
pitch / 4. - OCCT: Pure-Swift.
ThreadSpec.minorDiameter
Minor diameter, the inner diameter at the thread root (external) or crest (internal). Form-dependent via cutDepth.
public var minorDiameter: Double { get }
- Returns:
nominalDiameter - 2 * cutDepth. - OCCT: Pure-Swift.
- Example:
let m12 = ThreadSpec(form: .iso68, nominalDiameter: 12, pitch: 1.75) m12.theoreticalDepth // H ≈ 1.516 m12.cutDepth // 5H/8 ≈ 0.947 m12.minorDiameter // ≈ 10.106
Designation Parsing Internals
ThreadSpec.parse(_:) above dispatches to eight private static helpers. None of these is part of the public API: they exist only to keep parse(_:) itself short, but each is a real declaration in the source, documented here for completeness.
Five are per-family (parseMetric, parseTrapezoidal, parseAcme, parseUnified, parsePipeOrWhitworth) and three are shared: parseFractionOrDecimal reads 1/4 or 1.5, metricCoarsePitch(forDiameter:) supplies the pitch an M10 designation leaves out, and parseInchDesignation(_:form:) reads the "<diameter>-<threads per inch>" body that ACME and Unified spell identically (#989). ACME and Unified therefore differ only in the suffix each strips and the ThreadForm it stamps; the millimetre conversion, the fraction handling and the TPI > 0 check happen once. The conversion factor itself is the mmPerInch property rather than a literal repeated per call site.
ThreadProfile
A thread’s tooth cross-section over one pitch, normalised. Sendable, Hashable, Codable.
public struct ThreadProfile: Sendable, Hashable, Codable
axial runs 0…1 along the pitch; depth runs 0 (crest, at the major radius) … 1 (root, at the minor radius). Vertices are ordered by increasing axial; the profile is periodic (first.axial == 0, last.axial == 1, first.depth == last.depth) so consecutive teeth tile. The modeller maps a vertex to 3D as radius rMajor − depth × cutDepth, axial position axial × pitch, helix angle θ(z) + handed × axial × 2π.
ThreadProfile.Vertex
A single point in the normalised tooth outline.
public struct Vertex: Sendable, Hashable, Codable {
public var axial: Double // 0…1 along the pitch
public var depth: Double // 0 = crest (major R), 1 = root (minor R)
public init(axial: Double, depth: Double)
}
ThreadProfile.Vertex.axial
Position along one pitch, 0...1. Vertices are ordered by increasing axial; the profile is periodic (first.axial == 0, last.axial == 1) so consecutive teeth tile.
ThreadProfile.init?(vertices:)
Validates and creates a custom profile.
public init?(vertices: [Vertex])
Returns nil unless the vertices form a well-ordered, periodic, full-depth-spanning tooth outline: at least 3 vertices; first.axial ≈ 0, last.axial ≈ 1; first.depth ≈ last.depth; vertices are monotonically non-decreasing in axial; depth spans [0, 1] (must include a crest vertex at depth ≈ 0 and a root vertex at depth ≈ 1).
- Parameters:
vertices, ordered vertex list defining the tooth outline. - Returns: Valid
ThreadProfile, ornilif the outline violates the contract. - OCCT: Pure-Swift.
- Example:
guard let profile = ThreadProfile(vertices: [ .init(axial: 0.0, depth: 1), .init(axial: 0.1, depth: 1), // root flat .init(axial: 0.5, depth: 0), .init(axial: 0.6, depth: 0), // crest flat .init(axial: 0.9, depth: 1), .init(axial: 1.0, depth: 1), // root flat ]) else { return } // profile.supportsSmoothRodBuild == true
ThreadProfile.vertices
The ordered vertex list defining the tooth outline.
public let vertices: [Vertex]
ThreadProfile.SegmentKind
Segment classification for the modeller and cutter.
public enum SegmentKind: Sendable, Hashable {
case flat // constant depth → circular arc in 3D
case wall // constant axial → radial line (square thread walls)
case flank // sloped → sampled spline
}
ThreadProfile.Segment
A consecutive pair of vertices with its geometric classification.
public struct Segment: Sendable, Hashable {
public let a: Vertex, b: Vertex, kind: SegmentKind
}
| Field | Meaning |
|---|---|
a | Vertex at the start of the segment (lower axial). |
b | Vertex at the end of the segment (higher axial). |
kind | Geometric classification: .flat, .wall, or .flank (see SegmentKind above). |
ThreadProfile.Segment.a
ThreadProfile.segments
One segment per consecutive vertex pair, with SegmentKind classification.
public var segments: [Segment] { get }
- Returns: Array of
Segmentstructs;flatwherea.depth ≈ b.depth,wallwherea.axial ≈ b.axial,flankotherwise. - OCCT: Pure-Swift.
ThreadProfile.hasCrestFlat
true if the crest (depth ≈ 0) contains a real flat of non-zero axial width.
public var hasCrestFlat: Bool { get }
Pointed-crest profiles (a single vertex at depth = 0) return false and cannot use the smooth direct rod build path.
- Returns: Boolean indicating the presence of a usable crest flat.
- OCCT: Pure-Swift.
- Note: “is this segment a flat at this depth” is one internal predicate (
Segment.isFlat(atDepth:)), shared with the internalflatWidthFraction(atDepth:)the cut path sizes its groove from and with the crest lookup the direct rod build starts at (#991).
ThreadProfile.supportsSmoothRodBuild
Whether this profile can be built by the smooth, boolean-free direct rod path.
public var supportsSmoothRodBuild: Bool { get }
Requires a real crest flat (hasCrestFlat == true) and at most two flank segments. Pointed-crest or many-flank profiles (knuckle, sinusoidal) return false and must use the faceted boolean cut path instead. Consumed by Shape.threadedRod and the direct branch of Shape.threadedShaft.
- Returns:
hasCrestFlat && segments.filter { $0.kind == .flank }.count <= 2. - OCCT: Pure-Swift.
ThreadProfile.iso60V(crestFlatFraction:rootFlatFraction:)
ISO-68 / Unified 60° V profile.
public static func iso60V(crestFlatFraction: Double = 1.0 / 8,
rootFlatFraction: Double = 1.0 / 4) -> ThreadProfile
Symmetric truncated trapezoid: root half-flats at the ends, crest flat in the middle, straight 30° flanks between. Defaults reproduce the shipped ISO-68 geometry exactly (crest P/8, root P/4).
- Parameters:
crestFlatFraction, crest flat as a fraction of pitch;rootFlatFraction, root flat as a fraction of pitch. - Returns: A valid
ThreadProfile. - OCCT: Pure-Swift.
ThreadProfile.whitworth55
Whitworth / BSW / BSP 55° profile (flat-truncation, cutDepth = 0.640327 × P).
public static let whitworth55: ThreadProfile
Crest flat = root flat = P/6. The standard rounds the outer/inner sixth of the tooth; this is the flat-truncation of that form, which satisfies supportsSmoothRodBuild.
ThreadProfile.acme29
ACME 29° general-purpose profile (crest flat = root flat = 0.3707 × P at cutDepth = P/2).
public static let acme29: ThreadProfile
ThreadProfile.trapezoidalMetric30
ISO metric trapezoidal “Tr” 30° profile (crest flat = root flat = 0.366 × P at cutDepth = P/2).
public static let trapezoidalMetric30: ThreadProfile
ThreadProfile.square
Square thread profile, 0° radial walls, equal land and groove (cutDepth = P/2).
public static let square: ThreadProfile
Crest flat = root flat = P/2, which is the limiting case of the same truncated-trapezoid construction whitworth55, acme29 and trapezoidalMetric30 use: the flanks are left no axial run at all, so they classify as wall rather than flank (#988).
ThreadProfile.buttress
Buttress (DIN 513), asymmetric 3° load flank / 30° clearance flank, cutDepth = 0.86777 × P.
public static let buttress: ThreadProfile
The near-radial (3°) load flank rises steeply to the crest; the 30° clearance flank falls back to the root. Verified against the DIN 513 table (e.g. S 10×2 → d3 = 6.528).
Built by the same truncated-trapezoid construction as the symmetric forms, with the crest flat (0.26 × P) centred at 0.2722 × P rather than at mid-pitch; the off-centre crest is what makes the two flanks differ. Root flat = 0.1936 × P (#988).
ThreadProfile.knuckle
Knuckle / round thread (DIN 405): 30°-included (15° per side) flanks with circular-arc rounded crest and root, at standard depth 0.55 × P.
public static let knuckle: ThreadProfile
Small crest/root lands are kept so the smooth direct build can attach a crest flat. supportsSmoothRodBuild is true for this profile. Verified against the DIN 405 dimension table (bolt minor d3 = d − 1.1 × P).
ThreadForm
Which standard thread geometry to use. String, Sendable, Codable, CaseIterable.
public enum ThreadForm: String, Sendable, Codable, CaseIterable {
case iso68 // Metric M-series, 60° V
case unified // Unified (UNC / UNF / metric-fine / SAE), 60° V
case whitworth // BSW Whitworth, 55°
case bspParallel // BSP parallel "G", Whitworth 55° form
case acme // ACME general-purpose, 29° trapezoidal
case trapezoidal // ISO metric trapezoidal "Tr", 30°
case square // square / 0° walls
case buttress // asymmetric buttress, 7° load / 45° trailing
case knuckle // rounded / sinusoidal (DIN 405)
case nptTapered // NPT, 60° V on a 1:16 taper
case bsptTapered // BSPT, 55° on a 1:16 taper
case custom // arbitrary cross-section (see ThreadSpec.customProfile)
}
| Case | Form / standard | Angle | Notes |
|---|---|---|---|
.iso68 | Metric M-series | 60° V | ISO 68-1; coarse-pitch table in ThreadSpec.parse |
.unified | UNC / UNF / SAE / metric-fine | 60° V | Same form, just a pitch |
.whitworth | BSW / Whitworth | 55° | BS 84; flat-truncated crest/root |
.bspParallel | BSP “G” parallel | 55° | EN ISO 228 / BS 2779 |
.acme | ACME general-purpose | 29° | Power / lead screws |
.trapezoidal | ISO Tr metric | 30° | DIN 103 |
.square | Square | 0° | Equal land and groove |
.buttress | Buttress | 3°/30° | DIN 513 asymmetric |
.knuckle | Knuckle/round | 30° included, rounded | DIN 405 |
.nptTapered | NPT | 60° V on 1:16 taper | ANSI B1.20.1 |
.bsptTapered | BSPT | 55° on 1:16 taper | BS EN 10226 |
.custom | Arbitrary | , | Supply customProfile on ThreadSpec |
- OCCT: Pure-Swift enum; consumed by
ThreadSpec.profile,ThreadSpec.cutDepth, andThreadSpec.taperRatio. - Note:
.acme,.trapezoidal,.square,.buttress, and.knuckleare open for future tolerance-class (2B, 3A, etc.) parameters, those are fit-allowance tables, not form geometry, and are not currently modelled.
ThreadForm.custom
Arbitrary cross-section, supplied via ThreadSpec.customProfile.
ThreadBuild
Construction path selector for Shape.threadedShaft. Sendable, Hashable, Codable.
public enum ThreadBuild: Sendable, Hashable, Codable {
case auto
case direct
}
| Case | Behaviour |
|---|---|
.auto | Smooth boolean-free direct build for single- and multi-start coaxial cylinders (#213/#257); falls back to the boolean cut otherwise (non-cylinder / rounded / tapered). The recommended default. |
.direct | Prefer the smooth direct build; fall back to the boolean cut when unavailable (non-cylinder, rounded/tapered form, construction failure). Identical to .auto for coaxial cylinders. |
.boolean | Removed as a case in v2.0.0 (deprecated in #254; formerly forced the boolean cut path). Decoding still accepts the legacy "boolean" JSON key and resolves it to .direct. |
- Note (#222 / #232 / #254): the direct build’s crest sits at the nominal major radius, the earlier “overshoot” report was a
Bnd_Boxcontrol-hull artifact. Measure the true crest withboundingBoxOptimal()or mesh vertices (both read nominal);boundsover-reads by ~13–21% on the B-spline helicoid. There is no longer a reason to force the cut path for an “exact outer diameter”. - Multi-start (#257): single- and multi-start threads on a coaxial cylinder both build via the smooth direct path. The faceted cut path now only handles non-cylinder targets, rounded forms (knuckle / rounded Whitworth), and tapered pipe forms (NPT/BSPT).
ThreadBuild.auto
The recommended default: a smooth, boolean-free direct build for single- and multi-start threads on a plain coaxial cylinder, falling back to the boolean cut path otherwise (non-cylinder target, rounded or tapered form).
ThreadBuild.direct
Prefers the smooth direct build, falling back to the boolean cut path only when the direct build is unavailable (non-cylinder target, rounded/tapered form, or a construction failure). Identical to .auto for a coaxial cylinder.
- Note (#222 / #232 / #254): the direct build’s crest sits at the nominal major radius, the earlier “overshoot” report was a
Bnd_Boxcontrol-hull artifact. Measure the true crest withboundingBoxOptimal()or mesh vertices (both read nominal);boundsover-reads by ~13–21% on the B-spline helicoid. There is no longer a reason to force the cut path for an “exact outer diameter”. - Multi-start (#257): single- and multi-start threads on a coaxial cylinder both build via the smooth direct path. The faceted cut path now only handles non-cylinder targets, rounded forms (knuckle / rounded Whitworth), and tapered pipe forms (NPT/BSPT).
RunoutStyle
How a thread terminates at its ends. Sendable, Hashable.
public enum RunoutStyle: Sendable, Hashable {
case none
case filleted(radius: Double)
case tapered(turns: Double)
}
| Case | Behaviour |
|---|---|
.none | Hard-stop at each end (no runout). Cheap and exact but manufacturing-unrealistic. Default. |
.filleted(radius:) | Fillet the last turns’ worth of helix into the underlying surface, a post-boolean/sew fillet pass of the given radius. |
.tapered(turns:) | Taper the V-profile to zero depth over the last turns revolutions using a law-scaled sweep. Currently falls back to .filleted(radius: spec.pitch * 0.5) while law-scaling is not yet wrapped (#67). |
(Per-case anchors below, for cross-reference; the table above has the actual meaning of each.)
tapered
- OCCT:
.filleteddelegates toShape.filleted(radius:)(bridge:BRepFilletAPI_MakeFillet)..taperedis a planned law-scaled pipe-shell extension (issue #67); currently approximated by.filleted. - Example:
rod.threadedShaft(axisOrigin: .zero, axisDirection: SIMD3(0, 0, 1), spec: spec, length: 18, runout: .filleted(radius: 0.4))