Thread: the chamfered tip reaches ISO 724's external minor diameter

ISO 4753's chamfered end bounds the end's diameter by the minor diameter. The default took the
modelled root (the basic d1) for it, so the cone's small circle lay on the root cylinder and
the chamfer ring pinched to nothing wherever the groove reached the end. The default is now
the external minor diameter d3 = d - 1.226869 P of ISO 724, below d1: the cone crosses the
root, each end is a whole disk of d3 inside a whole conical ring. ISO 4753 is for ISO metric
threads; the other forms, whose root is already their design minor, give chamfer_depth.

Ladder 06/017's analytic volume follows (1945.354522563); its golden entry re-measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-27 01:58:51 -04:00
committed by Irakli Kourtis
parent d1a8f0417f
commit a30a85698f
6 changed files with 64 additions and 21 deletions
+18 -3
View File
@@ -593,8 +593,12 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
/// The chamfered tip of an external thread (ISO 4753, chamfered end CH) at each of its free ends:
/// the material outside a cone `angle` to the axis (default 45°) that reaches the diameter
/// d_f = d - 2 `depth` at the face's end (default: the external thread's minor diameter, the
/// largest ISO 4753 allows), as a ring to cut after the groove, so the last teeth taper into it.
/// d_f = d - 2 `depth` at the face's end, as a ring to cut after the groove, so the last teeth
/// taper into it. ISO 4753 bounds d_f by the external thread's minor diameter; the default is
/// that bound, d3 = d - 1.226869 P (ISO 724, the rounded root of the design profile). It lies
/// below the modelled basic root d1, so the cone crosses the root cylinder and leaves a whole
/// ring on the end; one ending on the root itself would pinch to nothing wherever the groove
/// reaches the end. ISO 4753 is for ISO metric threads: other forms give `depth`.
fn tip_cones(t: &ThreadInfo, radius: f64, ends: [End; 2], angle: Option<f64>, depth: Option<f64>) -> Result<Vec<Body>> {
if t.internal {
return Err(DocError::Invalid("a chamfered tip is for an external thread (ISO 4753); countersink the hole's entry instead".into()));
@@ -605,7 +609,18 @@ fn tip_cones(t: &ThreadInfo, radius: f64, ends: [End; 2], angle: Option<f64>, de
return Err(DocError::Invalid("the chamfer's angle to the axis must be between 1° and 89°".into()));
}
let r_top = radius.max(t.major_diameter / 2.0);
let depth = depth.unwrap_or(t.major_diameter / 2.0 - g.root_r);
let depth = match (depth, t.form) {
(Some(d), _) => d,
// ISO 724: d3 = d1 - H/6 = d - 2 (17/24) H.
(None, ThreadForm::Metric) => 17.0 / 24.0 * t.pitch * 3f64.sqrt() / 2.0,
(None, form) => {
return Err(DocError::Invalid(format!(
"ISO 4753's chamfered end is for ISO metric threads: give a {} thread its chamfer depth (its root is {} mm deep)",
form.name(),
show_mm(t.major_diameter / 2.0 - g.root_r)
)));
}
};
let r_f = t.major_diameter / 2.0 - depth;
if !(depth > 0.0 && r_f > 1e-3) {
return Err(DocError::Invalid(format!("the chamfer's depth must be between 0 and {} mm", show_mm(t.major_diameter / 2.0))));
+1 -1
View File
@@ -93,7 +93,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.thread", "Thread", thread)
.at("SOLID", "CREATE")
.icon("thread")
.params("face: [x,y,z] on a cylindrical face; form?: metric|trapezoidal|acme|buttress|buttress_asme (default: the one `designation` names, else metric; square is refused: no current standard defines it); designation?: the size, as its standard writes it: metric \"M8\", \"M8x1\", \"M16xPh3P1.5\" (ISO 68-1, ISO 261, ISO 965-1); trapezoidal \"Tr20x4\", \"Tr40x14(P7)\" (ISO 2904, ISO 2903); buttress \"S20x4\", \"S40x14(P7)\" (DIN 513, 3°/30°); acme \"1/2-10 ACME\" (ASME B1.5); buttress_asme \"2.5-8 BUTT\" (ASME B1.9, 7°/45°, basic profile; no size table) (default: the standard size that fits the face, a shaft its major diameter, a hole its drill); hand?: right|left|both (default right; both: a right- and a left-handed groove crossing); steep_flank?: start|end (buttress only, default end: the steep flank of the screw's teeth faces the thread's end, the way its axis points); starts?: 1..8 (lead = starts x pitch: that many grooves, each a turn's starts-th further round; a designation that gives the lead gives the starts); ease_in?, ease_out?: expr|standard|short (modelled threads: the groove fades out over this length, its section moving out of the material as it turns; standard 2.5 P, short 1.25 P per ISO 3508; ease_in at the end the thread starts from (`from`, or the start of its extent), ease_out at the other; it lies past the full-form length, up to a shoulder's clearance; at a free end, inside the face); tip?: none|chamfer (modelled external threads: each free end the thread reaches is chamfered after the groove is cut, ISO 4753's chamfered end, the last teeth tapering into the cone; chamfer_angle?: angle expr to the axis, default 45 deg; chamfer_depth?: expr, radial from the major diameter, default down to the external minor diameter, the largest ISO 4753 allows); extent?: full|length|up_to (default: length when `length` is given, up_to when `up_to` is, else full: the whole face); length?: expr (from the end the thread starts at); up_to?: [x,y,z] (a point, such as on a shoulder face: the thread runs to its place along the axis) | plane name or {origin, normal} (to where the plane crosses the axis); from?: [x,y,z] near the end of the face the thread starts at (default: the face's only free end, the one with no material past it; else the end the `face` point lies within a third of the face of; a pick in the middle of a face whose ends are alike is refused); modeled?: bool (default false). Cosmetic: the model is unchanged, model.threads lists it. Modeled: cuts the thread profile into the body: past a free end the groove runs out a pitch into the air (or through a chamfer); at a shoulder, a head or a blind hole's bottom it stops with its section clear of it, never cutting into it; a shaft thicker than the thread is refused with the size to make it, never turned down"),
.params("face: [x,y,z] on a cylindrical face; form?: metric|trapezoidal|acme|buttress|buttress_asme (default: the one `designation` names, else metric; square is refused: no current standard defines it); designation?: the size, as its standard writes it: metric \"M8\", \"M8x1\", \"M16xPh3P1.5\" (ISO 68-1, ISO 261, ISO 965-1); trapezoidal \"Tr20x4\", \"Tr40x14(P7)\" (ISO 2904, ISO 2903); buttress \"S20x4\", \"S40x14(P7)\" (DIN 513, 3°/30°); acme \"1/2-10 ACME\" (ASME B1.5); buttress_asme \"2.5-8 BUTT\" (ASME B1.9, 7°/45°, basic profile; no size table) (default: the standard size that fits the face, a shaft its major diameter, a hole its drill); hand?: right|left|both (default right; both: a right- and a left-handed groove crossing); steep_flank?: start|end (buttress only, default end: the steep flank of the screw's teeth faces the thread's end, the way its axis points); starts?: 1..8 (lead = starts x pitch: that many grooves, each a turn's starts-th further round; a designation that gives the lead gives the starts); ease_in?, ease_out?: expr|standard|short (modelled threads: the groove fades out over this length, its section moving out of the material as it turns; standard 2.5 P, short 1.25 P per ISO 3508; ease_in at the end the thread starts from (`from`, or the start of its extent), ease_out at the other; it lies past the full-form length, up to a shoulder's clearance; at a free end, inside the face); tip?: none|chamfer (modelled external threads: each free end the thread reaches is chamfered after the groove is cut, ISO 4753's chamfered end, the last teeth tapering into the cone; chamfer_angle?: angle expr to the axis, default 45 deg; chamfer_depth?: expr, radial from the major diameter, default for a metric thread down to the external minor diameter d3 = d - 1.226869 P (ISO 724), the largest ISO 4753 allows; other forms give it); extent?: full|length|up_to (default: length when `length` is given, up_to when `up_to` is, else full: the whole face); length?: expr (from the end the thread starts at); up_to?: [x,y,z] (a point, such as on a shoulder face: the thread runs to its place along the axis) | plane name or {origin, normal} (to where the plane crosses the axis); from?: [x,y,z] near the end of the face the thread starts at (default: the face's only free end, the one with no material past it; else the end the `face` point lies within a third of the face of; a pick in the middle of a face whose ends are alike is refused); modeled?: bool (default false). Cosmetic: the model is unchanged, model.threads lists it. Modeled: cuts the thread profile into the body: past a free end the groove runs out a pitch into the air (or through a chamfer); at a shoulder, a head or a blind hole's bottom it stops with its section clear of it, never cutting into it; a shaft thicker than the thread is refused with the size to make it, never turned down"),
CommandSpec::new("solid.shell", "Shell", shell)
.at("SOLID", "MODIFY")
.icon("shell")
+24 -5
View File
@@ -863,17 +863,26 @@ fn m10_stock(z0: f64, z1: f64, rmax: impl Fn(f64) -> f64) -> f64 {
(0..n).map(|i| z0 + (z1 - z0) * (i as f64 + 0.5) / n as f64).map(|z| 2.0 * PI * ring(rmax(z)) * (z1 - z0) / n as f64).sum()
}
/// ISO 4753's chamfered end on both free ends of an M10 stud: a 45° cone down to the minor
/// diameter, cut after the groove; the volume as the stock's chamfered section with the thread's
/// material fraction (as ladder 06/006).
/// ISO 4753's chamfered end on both free ends of an M10 stud: a 45° cone down to the external
/// minor diameter d3 (ISO 724), cut after the groove; the volume as the stock's chamfered section
/// with the thread's material fraction (as ladder 06/006). d3 lies below the modelled root, so
/// each end face is the whole disk of d3, with the cone a whole ring around it, not pinched
/// where the groove reaches the end.
#[test]
fn a_chamfered_tip_tapers_the_last_teeth() {
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius": 5, "height": 30, "body_name": "Stud"}));
run(&mut s, "solid.thread", json!({"face": [5, 0, 15], "designation": "M10", "tip": "chamfer", "modeled": true}));
let st = s.model.state();
assert!(st.body("Stud").unwrap().body.validity().is_empty());
let rf = 5.0 - 5.0 * 1.5 * 3f64.sqrt() / 16.0;
let stud = &st.body("Stud").unwrap().body;
assert!(stud.validity().is_empty());
let rf = 5.0 - 17.0 / 24.0 * 1.5 * 3f64.sqrt() / 2.0;
let ends: Vec<f64> =
stud.faces(1e-4).unwrap().into_iter().filter(|f| f.plane_normal.is_some_and(|n| n.z.abs() > 0.999)).map(|f| f.area).collect();
assert_eq!(ends.len(), 2);
for a in ends {
assert!((a - PI * rf * rf).abs() < 1e-3 * a, "each end is a whole disk of d3: {a} vs {}", PI * rf * rf);
}
let want = m10_stock(0.0, 30.0, |z| 5f64.min(rf + z).min(rf + 30.0 - z));
let got = exact_volume(&s, "Stud");
assert!((got - want).abs() < want * 1e-4, "{got} vs {want}");
@@ -899,6 +908,16 @@ fn a_chamfered_tip_tapers_the_last_teeth() {
.err()
.map(|e| e.to_string());
assert!(e.unwrap_or_default().contains("countersink"));
// ISO 4753 is for ISO metric threads: another form names its chamfer depth.
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"radius": 10, "height": 40, "body_name": "Screw"}));
let e = s
.execute("solid.thread", &json!({"face": [10, 0, 20], "designation": "Tr20x4", "tip": "chamfer", "modeled": true}))
.err()
.map(|e| e.to_string());
assert!(e.unwrap_or_default().contains("chamfer depth"));
run(&mut s, "solid.thread", json!({"face": [10, 0, 20], "designation": "Tr20x4", "tip": "chamfer", "chamfer_depth": 2.5, "modeled": true}));
assert!(s.model.state().body("Screw").unwrap().body.validity().is_empty());
}
/// M10's groove section moved `d` out from the axis, clipped at the shaft's radius 5: the cut per
+13 -4
View File
@@ -83,9 +83,13 @@ radially.
**Tip** (`tip: chamfer`, ISO 4753's chamfered end): at each free end of a modelled external
thread, after the groove, the material outside a cone `chamfer_angle` to the axis (45°) reaching
d_f = d − 2 `chamfer_depth` at the end (default the external minor diameter, the largest the
standard allows) is cut, the last teeth tapering into it. An internal thread takes the hole's
countersink instead.
d_f = d − 2 `chamfer_depth` at the end is cut, the last teeth tapering into it. ISO 4753 bounds
d_f by the minor diameter; the default is that bound, the external minor diameter d3 = d −
1.226869 P of ISO 724 (8.160 for M10), which lies below the modelled basic root d1. The cone
therefore crosses the root and leaves the end a whole disk of d3 inside a whole conical ring; a
cone ending on the root itself would pinch to nothing wherever the groove reaches the end. ISO
4753 is for ISO metric threads, so the other forms give `chamfer_depth`. An internal thread takes
the hole's countersink instead.
## Sources
@@ -104,4 +108,9 @@ countersink instead.
- ASME B1.9 (buttress inch): 7°/45°, basic height of engagement 0.6 P; the flat follows from the
fundamental triangle H = P / (tan 7° + tan 45°) = 0.89064 P truncated equally at crest and root.
- ISO 3508 (thread runouts) and ISO 4753 (ends of parts with external threads) for the runout and
the chamfered tip.
the chamfered tip. ISO 4753:2011, figure 1 b) (chamfered end CH; the public preview at
<https://cdn.standards.iteh.ai/samples/55928/38ddde12863b4c2db35dc929fd502a4d/ISO-4753-2011.pdf>):
the end's diameter "Maximum diameter at the minor thread diameter", the angle ≤ 50°, the
incomplete thread u ≤ 2 P.
- ISO 724 (external minor diameter d3 = d1 − H/6 = d − 1.226869 P, M10x1.5: 8.16), as given at
<https://www.valvias.com/miscellanea-metric-thread-din-13.php>.
@@ -1,10 +1,10 @@
{
"sources": [],
"what": "001's modelled M10 thread with ISO 4753's chamfered end at both free ends: a 45 deg cone down to the minor diameter, cut after the groove",
"what": "001's modelled M10 thread with ISO 4753's chamfered end at both free ends: a 45 deg cone down to the external minor diameter d3, cut after the groove",
"source": "analytic",
"method": "ISO 68-1 basic profile (60 deg, root flat P/4 at d - 5H/4 ... minor d1 = d - 1.0825 P, crest flat P/8): a helical groove of fixed section is a screw motion, so averaged over the turn every meridian half-plane holds the same section and V = 2 pi int int r m(r) dr dz over the stock's section, m the material fraction per pitch; the groove runs out past free ends. Tolerance 0.01%: SMLib's exact screw sweep; the chamfer is axisymmetric and cut after the groove, so it only lowers the stock's outer radius: r <= d_f/2 + distance from the end (45 deg), d_f the minor diameter d - 5H/4 = 8.376 (ISO 4753 chamfered end: the end's diameter at most the minor diameter)",
"method": "ISO 68-1 basic profile (60 deg, root flat P/4 at d - 5H/4 ... minor d1 = d - 1.0825 P, crest flat P/8): a helical groove of fixed section is a screw motion, so averaged over the turn every meridian half-plane holds the same section and V = 2 pi int int r m(r) dr dz over the stock's section, m the material fraction per pitch; the groove runs out past free ends. Tolerance 0.01%: SMLib's exact screw sweep; the chamfer is axisymmetric and cut after the groove, so it only lowers the stock's outer radius: r <= d_f/2 + distance from the end (45 deg), d_f the external minor diameter d3 = d - 1.226869 P = 8.160 (ISO 724; ISO 4753 chamfered end: the end's diameter at most the minor diameter), below the modelled root d1",
"body_count": 1,
"volume_mm3": 1947.833394596,
"volume_mm3": 1945.354522563,
"tolerance": {
"rel": 0.0001
}
+5 -5
View File
@@ -986,14 +986,14 @@
},
"ladder/06-threads/017-external-m10-chamfered-tip": {
"measured": {
"area_mm2": 1500.75072007837,
"area_mm2": 1492.9858770288445,
"body_count": 1,
"edges": 254,
"edges": 258,
"faces": 88,
"vertices": 168,
"volume_mm3": 1947.8329205613231
"vertices": 172,
"volume_mm3": 1945.3543465077544
},
"seconds": 3.485166487,
"seconds": 3.293720875,
"status": "pass"
},
"ladder/06-threads/018-external-m10-both-hands": {