mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Thread: the chamfered tip at the start or at both ends
`tip` was none|chamfer, chamfer cutting every free end. It is now none|start|both: `start` chamfers the end the thread starts from, refused when that end is not free (a shoulder, or a stop inside the face), and `both` every free end as `chamfer` did. The dialog's Tip pick offers None, Start and Both; `tip: chamfer` in a command names its replacements. The thread's end options (runouts, tip, chamfer angle and depth) came without a format change file; `pending/thread-ends.rs` records them and rewrites a saved `tip: chamfer` to `both`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,8 @@ pub(super) struct ThreadOn {
|
||||
steep: SteepFlank,
|
||||
/// Runout lengths (mm) at the thread's start and end along the axis.
|
||||
ease: [f64; 2],
|
||||
/// The end of the axis (0: its start, 1: its end) the thread starts from.
|
||||
from: usize,
|
||||
}
|
||||
|
||||
/// The free-end test of a face's two ends (`side` 0 at its start, 1 at its end), each made
|
||||
@@ -302,7 +304,7 @@ pub(super) fn thread_on(st: &ModelState, feature: u64, p: Vec3, ask: &ThreadAsk)
|
||||
hand: ask.hand,
|
||||
starts,
|
||||
};
|
||||
Ok(ThreadOn { info, cyl, ends, steep: ask.steep, ease })
|
||||
Ok(ThreadOn { info, cyl, ends, steep: ask.steep, ease, from: side })
|
||||
}
|
||||
|
||||
/// The end of the face (0: its start, 1: its end) a thread of limited length starts at: the end
|
||||
@@ -550,7 +552,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
return Err(DocError::Invalid("a runout is cut only on a modelled thread".into()));
|
||||
}
|
||||
let ask = ThreadAsk { ease: [runout(ease_in)?, runout(ease_out)?], ..ask };
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease } = thread_on(st, f.id, face.0, &ask)?;
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease, from: from_side } = thread_on(st, f.id, face.0, &ask)?;
|
||||
if !*modeled {
|
||||
st.threads.push(t);
|
||||
return Ok(());
|
||||
@@ -574,10 +576,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
let groove = crate::lineage::Lineage::unnamed_faces(groove_faces);
|
||||
let lineage = input.lineage.and_then(|l| crate::lineage::Lineage::step(&nb, &[&l, &groove]));
|
||||
let mut nb = Traced { body: nb, lineage };
|
||||
if *tip == ThreadTip::Chamfer {
|
||||
if *tip != ThreadTip::None {
|
||||
let angle = chamfer_angle.as_deref().map(|e| val(vals, e, Kind::Angle)).transpose()?;
|
||||
let depth = chamfer_depth.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?;
|
||||
for cone in tip_cones(&t, radius, ends, angle, depth)? {
|
||||
for cone in tip_cones(&t, radius, ends, (*tip, from_side), angle, depth)? {
|
||||
let faces = cone.face_count();
|
||||
let cut = kernel::boolean(&nb.body, &cone, BoolOp::Cut)?.ok_or_else(|| DocError::Invalid("the chamfer removed everything".into()))?;
|
||||
let unnamed = crate::lineage::Lineage::unnamed_faces(faces);
|
||||
@@ -591,7 +593,8 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The chamfered tip of an external thread (ISO 4753, chamfered end CH) at each of its free ends:
|
||||
/// The chamfered tip of an external thread (ISO 4753, chamfered end CH) at the free ends `tip`
|
||||
/// names (`Start`: the end of the axis `from`, which must be free; `Both`: each free end):
|
||||
/// 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, 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
|
||||
@@ -599,7 +602,14 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
/// 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>> {
|
||||
fn tip_cones(
|
||||
t: &ThreadInfo,
|
||||
radius: f64,
|
||||
ends: [End; 2],
|
||||
(tip, from): (ThreadTip, usize),
|
||||
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()));
|
||||
}
|
||||
@@ -633,8 +643,12 @@ fn tip_cones(t: &ThreadInfo, radius: f64, ends: [End; 2], angle: Option<f64>, de
|
||||
// nothing past a free end beyond the thread is touched.
|
||||
let e = 0.5 * t.pitch;
|
||||
let mut out = Vec::new();
|
||||
let free = |side: usize| ends.get(side) == Some(&End::Free);
|
||||
if tip == ThreadTip::Start && !free(from) {
|
||||
return Err(DocError::Invalid("the end the thread starts from is not a free end (material goes on past it, or the thread stops inside the face): chamfer it with `tip: both`, or start the thread from its free end".into()));
|
||||
}
|
||||
for (side, at, out_dir) in [(0usize, t.start, -k), (1, t.end, k)] {
|
||||
if ends.get(side) != Some(&End::Free) {
|
||||
if !free(side) || (tip == ThreadTip::Start && side != from) {
|
||||
continue;
|
||||
}
|
||||
// In the plane: x radial, y outward along the axis from the end.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//! A modelled thread saves how its ends are finished: `ease_in` and `ease_out`, the runout at the
|
||||
//! end it starts from and at the other (`standard` 2.5 P, `short` 1.25 P or `{length}`, ISO
|
||||
//! 3508), and `tip`, which free ends are chamfered (`start`: the end it starts from, `both`: each
|
||||
//! free end; ISO 4753), with `chamfer_angle` and `chamfer_depth`. Without them, as in an older
|
||||
//! design, the groove stops square and no end is chamfered. Designs saved while `tip` read
|
||||
//! `chamfer` (every free end) are rewritten as they open to `both`. The last release refuses a
|
||||
//! design saved with this (the newer format number).
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn step(v: &mut Value) -> crate::Result<()> {
|
||||
let Some(Value::Array(features)) = v.get_mut("features") else {
|
||||
return Ok(());
|
||||
};
|
||||
for feature in features {
|
||||
let Some(object) = feature.as_object_mut() else { continue };
|
||||
if object.get("type").and_then(Value::as_str) != Some("thread") {
|
||||
continue;
|
||||
}
|
||||
if object.get("tip").and_then(Value::as_str) == Some("chamfer") {
|
||||
object.insert("tip".into(), Value::String("both".into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn a_chamfered_tip_becomes_both_ends() {
|
||||
let mut v = json!({"features": [
|
||||
{"type": "thread", "designation": "M10", "modeled": true, "tip": "chamfer"},
|
||||
{"type": "thread", "designation": "M10", "modeled": true},
|
||||
{"type": "chamfer", "tip": "chamfer"}
|
||||
]});
|
||||
super::step(&mut v).unwrap();
|
||||
assert_eq!(v["features"][0]["tip"], json!("both"));
|
||||
assert!(v["features"][1].get("tip").is_none());
|
||||
assert_eq!(v["features"][2]["tip"], json!("chamfer"), "only threads");
|
||||
let mut empty = json!({"name": "Untitled"});
|
||||
super::step(&mut empty).unwrap();
|
||||
assert_eq!(empty, json!({"name": "Untitled"}));
|
||||
}
|
||||
}
|
||||
@@ -49,13 +49,17 @@ pub enum SteepFlank {
|
||||
End,
|
||||
}
|
||||
|
||||
/// The thread's free end: as cut, or chamfered (ISO 4753).
|
||||
/// Which of the thread's free ends are chamfered (ISO 4753's chamfered end).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ThreadTip {
|
||||
/// Every end as cut.
|
||||
#[default]
|
||||
None,
|
||||
Chamfer,
|
||||
/// The end the thread starts from, which must be free.
|
||||
Start,
|
||||
/// Every free end the thread reaches.
|
||||
Both,
|
||||
}
|
||||
|
||||
/// For `skip_serializing_if`: a field at its default is left out of the saved document.
|
||||
|
||||
@@ -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 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"),
|
||||
.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|start|both (modelled external threads: start chamfers the end the thread starts from, which must be free, both each free end the thread reaches, 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")
|
||||
@@ -2946,8 +2946,10 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let tip = match str_(p, "tip") {
|
||||
None | Some("none") => ThreadTip::None,
|
||||
Some("chamfer") => ThreadTip::Chamfer,
|
||||
Some(t) => return Err(bad(cmd, format!("unknown tip `{t}` (none or chamfer)"))),
|
||||
Some("start") => ThreadTip::Start,
|
||||
Some("both") => ThreadTip::Both,
|
||||
Some("chamfer") => return Err(bad(cmd, "tip `chamfer` is now `both` (every free end) or `start` (the end the thread starts from)")),
|
||||
Some(t) => return Err(bad(cmd, format!("unknown tip `{t}` (none, start or both)"))),
|
||||
};
|
||||
let angled = |key: &str, kind: Kind| -> Result<Option<solidcraft_doc::Expr>> {
|
||||
let e = expr(p, key);
|
||||
@@ -2959,7 +2961,7 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let chamfer_angle = angled("chamfer_angle", Kind::Angle)?;
|
||||
let chamfer_depth = angled("chamfer_depth", Kind::Length)?;
|
||||
if tip == ThreadTip::None && (chamfer_angle.is_some() || chamfer_depth.is_some()) {
|
||||
return Err(bad(cmd, "`chamfer_angle` and `chamfer_depth` go with `tip: chamfer`"));
|
||||
return Err(bad(cmd, "`chamfer_angle` and `chamfer_depth` go with a chamfered tip (`tip: start` or `both`)"));
|
||||
}
|
||||
let ease = |key: &str| -> Result<Option<solidcraft_doc::ThreadRunout>> {
|
||||
Ok(match p.get(key) {
|
||||
|
||||
@@ -553,7 +553,7 @@ fn a_thread_on_a_boss_never_cuts_into_the_plate_below() {
|
||||
let got = exact_volume(&s, "Part");
|
||||
assert!((got - want).abs() < want * 2e-4, "{got} vs {want}");
|
||||
assert!((plate_left(s) - 9600.0).abs() < 1e-6);
|
||||
for extra in [json!({"ease_out": "standard"}), json!({"ease_out": "short", "tip": "chamfer"})] {
|
||||
for extra in [json!({"ease_out": "standard"}), json!({"ease_out": "short", "tip": "both"})] {
|
||||
let s = boss(extra.clone());
|
||||
assert!(s.model.state().body("Part").unwrap().body.validity().is_empty(), "{extra}");
|
||||
let left = plate_left(s);
|
||||
@@ -872,7 +872,7 @@ fn m10_stock(z0: f64, z1: f64, rmax: impl Fn(f64) -> f64) -> f64 {
|
||||
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}));
|
||||
run(&mut s, "solid.thread", json!({"face": [5, 0, 15], "designation": "M10", "tip": "both", "modeled": true}));
|
||||
let st = s.model.state();
|
||||
let stud = &st.body("Stud").unwrap().body;
|
||||
assert!(stud.validity().is_empty());
|
||||
@@ -891,7 +891,7 @@ fn a_chamfered_tip_tapers_the_last_teeth() {
|
||||
run(
|
||||
&mut s,
|
||||
"solid.thread",
|
||||
json!({"face": [5, 0, 25], "designation": "M10", "tip": "chamfer", "chamfer_angle": "30 deg", "chamfer_depth": 0.5, "modeled": true}),
|
||||
json!({"face": [5, 0, 25], "designation": "M10", "tip": "both", "chamfer_angle": "30 deg", "chamfer_depth": 0.5, "modeled": true}),
|
||||
);
|
||||
let w = ext_half_width(10.0, 1.5);
|
||||
let rf = 4.5;
|
||||
@@ -903,23 +903,42 @@ fn a_chamfered_tip_tapers_the_last_teeth() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"corner": [-10, -10, 0], "length": 20, "width": 20, "height": 10, "body_name": "Nut"}));
|
||||
run(&mut s, "solid.hole", json!({"position": [0, 0, 10], "diameter": 8.5}));
|
||||
let e = s
|
||||
.execute("solid.thread", &json!({"face": [4.25, 0, 5], "designation": "M10", "tip": "chamfer", "modeled": true}))
|
||||
.err()
|
||||
.map(|e| e.to_string());
|
||||
let e =
|
||||
s.execute("solid.thread", &json!({"face": [4.25, 0, 5], "designation": "M10", "tip": "both", "modeled": true})).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}))
|
||||
.execute("solid.thread", &json!({"face": [10, 0, 20], "designation": "Tr20x4", "tip": "both", "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}));
|
||||
run(&mut s, "solid.thread", json!({"face": [10, 0, 20], "designation": "Tr20x4", "tip": "both", "chamfer_depth": 2.5, "modeled": true}));
|
||||
assert!(s.model.state().body("Screw").unwrap().body.validity().is_empty());
|
||||
}
|
||||
|
||||
/// `tip: start` chamfers only the end the thread starts from: on a stud started from its top, the
|
||||
/// top is chamfered down to d3 and the bottom stays as cut. A start against a shoulder is no free
|
||||
/// end and is refused; `both` there chamfers the one free end.
|
||||
#[test]
|
||||
fn a_tip_at_the_start_chamfers_only_that_end() {
|
||||
let rf = 5.0 - 17.0 / 24.0 * 1.5 * 3f64.sqrt() / 2.0;
|
||||
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": "start", "from": [5, 0, 30], "modeled": true}));
|
||||
assert!(s.model.state().body("Stud").unwrap().body.validity().is_empty());
|
||||
let want = m10_stock(0.0, 30.0, |z| 5f64.min(rf + 30.0 - z));
|
||||
let got = exact_volume(&s, "Stud");
|
||||
assert!((got - want).abs() < want * 1e-4, "{got} vs {want}");
|
||||
let mut s = shaft_on_collar();
|
||||
let e = s.execute("solid.thread", &json!({"face": [5, 0, 25], "designation": "M10", "tip": "start", "from": [5, 0, 10], "modeled": true}));
|
||||
assert!(e.err().map(|e| e.to_string()).unwrap_or_default().contains("not a free end"));
|
||||
run(&mut s, "solid.thread", json!({"face": [5, 0, 25], "designation": "M10", "tip": "both", "from": [5, 0, 10], "modeled": true}));
|
||||
let e = s.execute("solid.thread", &json!({"face": [5, 0, 25], "designation": "M10", "tip": "chamfer", "modeled": true}));
|
||||
assert!(e.err().map(|e| e.to_string()).unwrap_or_default().contains("now `both`"));
|
||||
}
|
||||
|
||||
/// M10's groove section moved `d` out from the axis, clipped at the shaft's radius 5: the cut per
|
||||
/// radian of turn, ∫ r w(r - d) dr from its moved root out to 5 (w the section's own width at
|
||||
/// its radius, P/4 + 2 (ρ - root) tan 30°). A cubic in d until the root reaches the surface.
|
||||
|
||||
@@ -176,7 +176,8 @@ const EXTENTS: &[(&str, &str)] = &[("Full", "full"), ("Length", "length"), ("Up
|
||||
/// Runout choices (ISO 3508); the last one is a typed length.
|
||||
const EASES: &[(&str, &str)] = &[("None", ""), ("Standard (2.5 P)", "standard"), ("Short (1.25 P)", "short"), ("Length", "")];
|
||||
const EASE_LENGTH: usize = 3;
|
||||
const TIPS: &[(&str, ThreadTip)] = &[("None", ThreadTip::None), ("Chamfer", ThreadTip::Chamfer)];
|
||||
/// Which free ends are chamfered (ISO 4753).
|
||||
const TIPS: &[(&str, ThreadTip)] = &[("None", ThreadTip::None), ("Start", ThreadTip::Start), ("Both", ThreadTip::Both)];
|
||||
const FLANKS: &[(&str, SteepFlank)] = &[("End", SteepFlank::End), ("Start", SteepFlank::Start)];
|
||||
|
||||
/// The pick of the end the thread is measured from (`from`).
|
||||
@@ -404,7 +405,7 @@ pub(crate) fn rows(
|
||||
enter |= ease_rows(ui, "Ease In", "thread_ease_in", &mut o.ease_in);
|
||||
enter |= ease_rows(ui, "Ease Out", "thread_ease_out", &mut o.ease_out);
|
||||
pick(ui, "Tip", "thread_tip", &mut o.tip, TIPS);
|
||||
if o.tip == ThreadTip::Chamfer {
|
||||
if o.tip != ThreadTip::None {
|
||||
enter |= hint_row(ui, "Chamfer Angle", &mut o.chamfer_angle, tr("ui.iso_4753"));
|
||||
enter |= hint_row(ui, "Chamfer Depth", &mut o.chamfer_depth, tr("ui.iso_4753"));
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ fn every_option_reaches_the_command() {
|
||||
choose(&mut h, "Ease In", "Standard (2.5 P)");
|
||||
choose(&mut h, "Ease Out", "Length");
|
||||
type_in(&mut h, "Ease Out Length", "1.5 mm");
|
||||
choose(&mut h, "Tip", "Chamfer");
|
||||
choose(&mut h, "Tip", "Both");
|
||||
type_in(&mut h, "Chamfer Angle", "30 deg");
|
||||
type_in(&mut h, "Chamfer Depth", "0.5 mm");
|
||||
click(&mut h, &["dialog:Modeled"]);
|
||||
@@ -86,7 +86,7 @@ fn every_option_reaches_the_command() {
|
||||
p,
|
||||
json!({
|
||||
"designation": "Tr20x4", "form": "trapezoidal", "hand": "left", "starts": 2, "extent": "length", "length": "6 mm",
|
||||
"ease_in": "standard", "ease_out": "1.5 mm", "tip": "chamfer", "chamfer_angle": "30 deg", "chamfer_depth": "0.5 mm",
|
||||
"ease_in": "standard", "ease_out": "1.5 mm", "tip": "both", "chamfer_angle": "30 deg", "chamfer_depth": "0.5 mm",
|
||||
"modeled": true,
|
||||
})
|
||||
);
|
||||
@@ -133,7 +133,7 @@ fn only_the_fields_that_apply_are_shown() {
|
||||
choose(&mut h, "Ease In", "Length");
|
||||
assert_eq!(inputs(&h), ["Cylindrical face", START_END], "a runout starts at an end");
|
||||
assert!(shown("Ease In Length") && !shown("Ease Out Length"));
|
||||
choose(&mut h, "Tip", "Chamfer");
|
||||
choose(&mut h, "Tip", "Start");
|
||||
assert!(shown("Chamfer Angle") && shown("Chamfer Depth"));
|
||||
let p = params(&h);
|
||||
assert!(p.get("chamfer_angle").is_none() && p.get("chamfer_depth").is_none(), "empty values take ISO 4753's: {p}");
|
||||
@@ -246,7 +246,7 @@ fn a_saved_thread_reopens_with_every_option() {
|
||||
for (made, picks) in [
|
||||
(
|
||||
json!({"designation":"S20x4","steep_flank":"start","hand":"left","starts":2,"length":"12 mm","from":[10,0,20],"modeled":true,
|
||||
"ease_in":"standard","ease_out":"1 mm","tip":"chamfer","chamfer_angle":"30 deg","chamfer_depth":"0.5 mm"}),
|
||||
"ease_in":"standard","ease_out":"1 mm","tip":"start","chamfer_angle":"30 deg","chamfer_depth":"0.5 mm"}),
|
||||
vec!["Cylindrical face", START_END],
|
||||
),
|
||||
(json!({"designation":"M20","from":[10,0,20],"up_to":[10,0,5]}), vec!["Cylindrical face", START_END, UP_TO]),
|
||||
|
||||
+2
-2
@@ -81,8 +81,8 @@ reaching nothing at the end. No end gets a runout unless asked. Where the sectio
|
||||
sweep's walls are within its tolerance (1e-4 P) of the true surfaces; full-depth turns are exact
|
||||
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
|
||||
**Tip** (ISO 4753's chamfered end; `tip: start` the end the thread starts from, which must be
|
||||
free, `tip: both` each free end it reaches): at those ends 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 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
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
],
|
||||
"modeled": true,
|
||||
"designation": "M10",
|
||||
"tip": "chamfer"
|
||||
"tip": "both"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user