mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Threads with several starts
Ladder 06/008 had no way to ask for one: "solid.thread has no start count". `starts: n` (1 to 8) cuts n grooves of lead n x P, the j-th a j/n turn round from the first (the same helix j pitches along), in one boolean with their union. A designation that gives the lead (M16xPh3P1.5, Tr40x14(P7)) gives the starts, and a `starts` that disagrees is refused. The helix's self-clearance check and the turn limit use the lead. Over the same length n grooves of lead nP take the volume of one start of pitch P: engine test (2 and 3 starts, M10xPh3P1.5) within 1e-4 of the closed form, ladder 06/008 graded as 001 (measured 3.5e-7 off). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,12 @@ pub(super) fn thread_on(st: &ModelState, feature: u64, p: Vec3, ask: &ThreadAsk)
|
||||
}
|
||||
};
|
||||
let (major, pitch) = (size.major, size.pitch);
|
||||
// A designation that gives the lead (M16xPh3P1.5, Tr40x14(P7)) gives the starts.
|
||||
let named = (size.lead / pitch).round() as u32;
|
||||
if ask.starts > 1 && named != starts {
|
||||
return Err(DocError::Invalid(format!("`{}` has {named} start(s), not {starts}", ask.designation.unwrap_or_default())));
|
||||
}
|
||||
let starts = named.max(1);
|
||||
let designation = form.format(major, pitch, size.lead);
|
||||
let minor = 2.0 * form.groove(major, pitch, true).crest_r;
|
||||
let fits = if cyl.internal {
|
||||
@@ -436,9 +442,6 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
if *tip != ThreadTip::None {
|
||||
return Err(DocError::Invalid("a chamfered thread tip is not built yet".into()));
|
||||
}
|
||||
if *starts > 1 {
|
||||
return Err(DocError::Invalid("a thread with several starts is not built yet".into()));
|
||||
}
|
||||
let extent = match (length, up_to) {
|
||||
(Some(e), _) => Extent::Length(val(vals, e, Kind::Length)?),
|
||||
(None, Some(crate::ThreadLimit::Face(q))) => Extent::UpToPoint(q.0),
|
||||
|
||||
@@ -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; not built yet); ease_in?, ease_out?: expr|standard|short (runout, not built yet); tip?: none|chamfer (with chamfer_angle?, chamfer_depth?; not built yet); 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 first turned down to its major diameter over the threaded length"),
|
||||
.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 (runout, not built yet); tip?: none|chamfer (with chamfer_angle?, chamfer_depth?; not built yet); 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 first turned down to its major diameter over the threaded length"),
|
||||
CommandSpec::new("solid.shell", "Shell", shell)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("shell")
|
||||
|
||||
@@ -897,3 +897,30 @@ fn a_square_thread_is_refused() {
|
||||
let e = s.execute("solid.thread", &json!({"face": [10, 0, 10], "form": "square", "designation": "20x4"})).err().map(|e| e.to_string());
|
||||
assert!(e.unwrap_or_default().contains("no current standard defines a square thread"));
|
||||
}
|
||||
|
||||
/// Ladder 06/008: n starts are n grooves of lead n P, each a turn's n-th further round: over the
|
||||
/// same length they take n x L/(nP) turns, the volume of one start of that pitch, and do not
|
||||
/// meet. A designation that gives the lead gives the starts.
|
||||
#[test]
|
||||
fn a_multi_start_thread_takes_the_volume_of_one_start() {
|
||||
let (d, p, len) = (10.0, 1.5, 30.0);
|
||||
let want = len / p * ext_turn(d, p, d / 2.0);
|
||||
for (starts, designation) in [(2, "M10"), (3, "M10"), (1, "M10xPh3P1.5")] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 5, "height": len, "body_name": "Shaft"}));
|
||||
let before = exact_volume(&s, "Shaft");
|
||||
run(&mut s, "solid.thread", json!({"face": [5, 0, 15], "designation": designation, "starts": starts, "modeled": true}));
|
||||
let st = s.model.state();
|
||||
assert!(st.body("Shaft").unwrap().body.validity().is_empty(), "{designation} x{starts}");
|
||||
let got = before - exact_volume(&s, "Shaft");
|
||||
assert!((got - want).abs() < want * 1e-4, "{designation} x{starts}: removed {got}, one start {want}");
|
||||
}
|
||||
// The cosmetic thread says it: two starts, lead 3.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 20, "height": 40}));
|
||||
let th = run(&mut s, "solid.thread", json!({"face": [20, 0, 20], "designation": "Tr40x14(P7)"}));
|
||||
assert_eq!(th["thread"]["designation"], "Tr40x14(P7)", "{th}");
|
||||
assert_eq!(th["thread"]["starts"], 2, "{th}");
|
||||
let e = s.execute("solid.thread", &json!({"face": [20, 0, 20], "designation": "Tr40x14(P7)", "starts": 3})).err().map(|e| e.to_string());
|
||||
assert!(e.unwrap_or_default().contains("has 2 start(s), not 3"));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"sources": [],
|
||||
"what": "a two-start M10 thread (lead 3, pitch 1.5)",
|
||||
"source": "invariants",
|
||||
"method": "would be graded as 001 with the material fraction of two grooves per lead",
|
||||
"missing": "solid.thread has no start count: multi-start threads"
|
||||
"what": "a two-start M10 thread (lead 3, pitch 1.5) over a d10 x 30 cylinder",
|
||||
"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; two grooves of lead 2P, the second a half turn round from the first: every meridian half-plane holds the same sections a pitch apart as 001's one groove, so the material fraction and the volume are 001's",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 1954.24591725,
|
||||
"tolerance": {
|
||||
"rel": 0.0001
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
{
|
||||
"commands": []
|
||||
"commands": [
|
||||
{
|
||||
"command": "solid.cylinder",
|
||||
"params": {
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"radius": 5,
|
||||
"height": 30,
|
||||
"body_name": "Part"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "solid.thread",
|
||||
"params": {
|
||||
"face": [
|
||||
5,
|
||||
0,
|
||||
15
|
||||
],
|
||||
"modeled": true,
|
||||
"designation": "M10",
|
||||
"starts": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -398,8 +398,11 @@ case(T6, "007-external-m10-at-filleted-shoulder", "a modelled M10 thread on a d1
|
||||
"the fillet's ring: the corner square [5,6]x[10,11] less the quarter disc about (6,11), V_f = 2 pi (5.5 - pi/4 (6 - 4/(3 pi))), its centroid z from the same moments; "
|
||||
"the cylinder face starts at z=11, where the fillet meets it. " + SHOULDER.replace("lower end is a shoulder (the d16 collar", "lower end (z=11) is a shoulder (the fillet and the collar") +
|
||||
"V = pi 8^2 10 + V_f + pi 5^2 (1 + w) + 2 pi int_{11+w}^35 int r m(r) dr dz. Tolerance 0.05%: SMLib's exact screw sweep")
|
||||
missing(T6, "008-multi-start-thread", "a two-start M10 thread (lead 3, pitch 1.5)", "solid.thread has no start count: multi-start threads",
|
||||
"would be graded as 001 with the material fraction of two grooves per lead")
|
||||
case(T6, "008-multi-start-thread", "a two-start M10 thread (lead 3, pitch 1.5) over a d10 x 30 cylinder", cylT + [thr([5, 0, 15], designation="M10", starts=2)],
|
||||
{"body_count": 1, "volume_mm3": ext_volume(10, 1.5, [(0, 30, lambda z: 5)]), "tolerance": {"rel": 1e-4}},
|
||||
TMETH.replace("Tolerance 0.5%: solid.thread cuts faceted by design (chords within P/200 on the body, P/100 on the tool)", "Tolerance 0.01%: SMLib's exact screw sweep") +
|
||||
"; two grooves of lead 2P, the second a half turn round from the first: every meridian half-plane holds the same sections a pitch apart as 001's one groove, "
|
||||
"so the material fraction and the volume are 001's")
|
||||
missing(T6, "009-thread-runout-length", "an M10 thread that vanishes over a 2-pitch runout before a shoulder", "solid.thread has no runout (vanishing thread) length",
|
||||
"a runout would let 004's thread vanish before the shoulder instead of stopping square")
|
||||
case(T6, "010-part-threaded-boss", "the running part with a modelled M16 thread on its d16 boss",
|
||||
|
||||
Reference in New Issue
Block a user