mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Tapped holes: Rc and NPT, and a modelled thread with the thread's End
The tapped-hole catalogue takes ISO 7-1's Rc and ASME B1.20.1's NPT internal taper threads, every size of the tables a modelled thread cuts. Their drill is the internal minor diameter where the thread ends (the reference plane's minor at the face less the 1 in 16 taper over the useful or effective thread length), derived from the standards rather than a tap maker's table. An external R is still no hole's. A tapped hole's options mirror the thread's: `modeled` cuts its thread into the wall, from the entry, by the same cut as `solid.thread` (now one function both call), and `ease_out` fades its inner end out before the bottom (ISO 3508); without it the groove stops square at the thread depth, clear of a blind hole's bottom. The Hole dialog's Tapped rows add Rc and NPT, Modeled and the End choice (Square, Fade, Fade Length). The format notes in pending/thread-ends.rs and pending/pipe-threads.rs record the new keys and sizes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -831,6 +831,14 @@ pub enum FeatureKind {
|
||||
/// The tapped thread's hand: right (the default) or left; never both.
|
||||
#[serde(default, skip_serializing_if = "crate::thread_forms::is_default")]
|
||||
hand: crate::thread_forms::ThreadHand,
|
||||
/// The tapped thread cut into the wall (the groove between its teeth, as a modelled
|
||||
/// `solid.thread` cuts it) instead of only listed.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
modeled: bool,
|
||||
/// A modelled tapped thread's inner end, towards the bottom: it fades out over this
|
||||
/// length (ISO 3508's runout); none: it stops square, or clear of a blind hole's bottom.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
ease_out: Option<ThreadRunout>,
|
||||
/// To Object: drill down to the face (or point) here instead of `depth`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
to: Option<FacePick>,
|
||||
|
||||
+26
-3
@@ -6285,11 +6285,17 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let extra: Vec<String> = if *operation == Operation::Join { to_body.into_iter().collect() } else { Vec::new() };
|
||||
apply_traced_with(doc, st, f, tools, *operation, &crate::plain(targets), &extra)
|
||||
}
|
||||
FeatureKind::Hole { thread, thread_depth, diameter, hand, .. } => {
|
||||
FeatureKind::Hole { thread, thread_depth, diameter, hand, modeled, ease_out, .. } => {
|
||||
let thread_depth = thread_depth.as_ref().map(|d| val(vals, d, Kind::Length)).transpose()?;
|
||||
if thread_depth.is_some() && thread.is_none() {
|
||||
return Err(DocError::Invalid("thread depth requires a tapped hole".into()));
|
||||
}
|
||||
if *modeled && thread.is_none() {
|
||||
return Err(DocError::Invalid("a modelled thread needs a tapped hole".into()));
|
||||
}
|
||||
if ease_out.is_some() && !*modeled {
|
||||
return Err(DocError::Invalid("a runout is cut only on a modelled thread".into()));
|
||||
}
|
||||
if thread_depth.is_some_and(|d| !d.is_finite() || d <= 0.0) {
|
||||
return Err(DocError::Invalid("thread depth must be finite and positive".into()));
|
||||
}
|
||||
@@ -6305,14 +6311,31 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
if let Some(d) = thread {
|
||||
// Find the bore beyond any entry recess, including shallow and through holes.
|
||||
let r = val(vals, diameter, Kind::Length)? / 2.0;
|
||||
let parallel_pipe = crate::pipe_thread_catalog::parse_pipe_thread(d).is_some();
|
||||
// A pipe thread's wall must be found: no fallback place on a guessed wall.
|
||||
let pipe = crate::parse_tapped_thread(d).is_some_and(|t| {
|
||||
matches!(
|
||||
t.family,
|
||||
crate::TappedThreadFamily::G | crate::TappedThreadFamily::Rp | crate::TappedThreadFamily::Rc | crate::TappedThreadFamily::Npt
|
||||
)
|
||||
});
|
||||
let ease = thread::runout(vals, ease_out)?;
|
||||
for p in spots {
|
||||
let wall = if parallel_pipe {
|
||||
let wall = if pipe {
|
||||
parallel_tapped_wall(st, p, dir, r).ok_or_else(|| DocError::Invalid("no cylindrical pipe-thread wall remains".into()))?
|
||||
} else {
|
||||
parallel_tapped_wall(st, p, dir, r).unwrap_or(p + dir.any_perp() * r + dir * (r * 0.5))
|
||||
};
|
||||
let ask = thread::ThreadAsk { designation: Some(d), tapped: true, hand: *hand, ..Default::default() };
|
||||
if *modeled {
|
||||
// Cut from the entry, as deep as `thread_depth` or along the whole wall:
|
||||
// at a blind hole's bottom the groove stops clear of it, or fades out
|
||||
// before it with a runout.
|
||||
let extent = thread_depth.map_or(thread::Extent::Full, thread::Extent::Length);
|
||||
let ask = thread::ThreadAsk { modeled: true, extent, from: Some(p), ease_out: ease, ..ask };
|
||||
let on = thread_on(st, f.id, wall, &ask)?;
|
||||
thread::cut(st, on, (crate::thread_forms::ThreadTip::None, None, None))?;
|
||||
continue;
|
||||
}
|
||||
let mut t = thread_on(st, f.id, wall, &ask)?.info;
|
||||
if let Some(depth) = thread_depth {
|
||||
let available = t.end - t.start;
|
||||
|
||||
@@ -87,7 +87,7 @@ pub(super) struct ThreadAsk<'a> {
|
||||
/// is mm + pitches x the pitch. The start never fades: it runs off its free end.
|
||||
pub ease_out: (f64, f64),
|
||||
/// A tapped hole's thread: `designation` is a catalogue size (ISO metric, or a catalogued
|
||||
/// UNC/UNF, G or Rp size), and a size that is not metric fits the hole drilled for it.
|
||||
/// UNC/UNF, G, Rp, Rc or NPT size), and a size that is not metric fits the hole drilled for it.
|
||||
pub tapped: bool,
|
||||
}
|
||||
|
||||
@@ -185,10 +185,9 @@ pub(super) fn thread_on(st: &ModelState, feature: u64, p: Vec3, ask: &ThreadAsk)
|
||||
let tapped = match (ask.tapped, ask.designation) {
|
||||
(false, _) => None,
|
||||
(true, None) => return Err(DocError::Invalid("a tapped hole needs a designation".into())),
|
||||
(true, Some(name)) => Some(
|
||||
crate::parse_tapped_thread(name)
|
||||
.ok_or_else(|| DocError::Invalid(format!("unknown tapped thread `{name}` (ISO metric or a catalogued UNC/UNF, G or Rp size)")))?,
|
||||
),
|
||||
(true, Some(name)) => Some(crate::parse_tapped_thread(name).ok_or_else(|| {
|
||||
DocError::Invalid(format!("unknown tapped thread `{name}` (ISO metric or a catalogued UNC/UNF, G, Rp, Rc or NPT size)"))
|
||||
})?),
|
||||
};
|
||||
let size = match (&tapped, ask.designation) {
|
||||
(Some(t), _) => crate::thread_forms::ThreadSize { form, major: t.major_diameter, pitch: t.pitch, lead: t.pitch },
|
||||
@@ -707,24 +706,35 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
ease_out: (0.0, 0.0),
|
||||
tapped: false,
|
||||
};
|
||||
// ISO 3508's runouts: standard 2.5 P, short 1.25 P.
|
||||
let runout = |e: &Option<crate::ThreadRunout>| -> Result<(f64, f64)> {
|
||||
Ok(match e {
|
||||
None => (0.0, 0.0),
|
||||
Some(crate::ThreadRunout::Standard) => (0.0, 2.5),
|
||||
Some(crate::ThreadRunout::Short) => (0.0, 1.25),
|
||||
Some(crate::ThreadRunout::Length { length }) => (val(vals, length, Kind::Length)?, 0.0),
|
||||
})
|
||||
};
|
||||
if ease_out.is_some() && !*modeled {
|
||||
return Err(DocError::Invalid("a runout is cut only on a modelled thread".into()));
|
||||
}
|
||||
let ask = ThreadAsk { ease_out: runout(ease_out)?, ..ask };
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease, from: from_side } = thread_on(st, f.id, face.0, &ask)?;
|
||||
let ask = ThreadAsk { ease_out: runout(vals, ease_out)?, ..ask };
|
||||
let on = thread_on(st, f.id, face.0, &ask)?;
|
||||
if !*modeled {
|
||||
st.threads.push(t);
|
||||
st.threads.push(on.info);
|
||||
return Ok(());
|
||||
}
|
||||
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()?;
|
||||
cut(st, on, (*tip, angle, depth))
|
||||
}
|
||||
|
||||
/// A runout as (mm, pitches): ISO 3508's standard 2.5 P and short 1.25 P, or a length.
|
||||
pub(super) fn runout(vals: &BTreeMap<String, Value>, e: &Option<crate::ThreadRunout>) -> Result<(f64, f64)> {
|
||||
Ok(match e {
|
||||
None => (0.0, 0.0),
|
||||
Some(crate::ThreadRunout::Standard) => (0.0, 2.5),
|
||||
Some(crate::ThreadRunout::Short) => (0.0, 1.25),
|
||||
Some(crate::ThreadRunout::Length { length }) => (val(vals, length, Kind::Length)?, 0.0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Cut a modelled thread resolved on its face into its body: the grooves (SMLib's exact screw
|
||||
/// sweeps, a taper's crests by its envelope) by one B-rep boolean, then the chamfered tip
|
||||
/// (`tip`, with its angle and depth when given).
|
||||
pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (ThreadTip, Option<f64>, Option<f64>)) -> Result<()> {
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease, from: from_side } = on;
|
||||
let i = st.bodies.iter().position(|b| b.name == t.body).ok_or_else(|| DocError::Invalid("threaded body".into()))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let (input, radius) = (mb.traced(), cyl.radius);
|
||||
@@ -758,10 +768,8 @@ 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::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, (*tip, from_side), angle, depth, taper)? {
|
||||
if tip != ThreadTip::None {
|
||||
for cone in tip_cones(&t, radius, ends, (tip, from_side), angle, depth, taper)? {
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! A thread's `form` may be `bspp` (ISO 228-1 parallel pipe thread, `G 1/2`), `rp` (ISO 7-1's
|
||||
//! parallel internal thread, `Rp 1/2`), `bspt` (ISO 7-1
|
||||
//! taper, `R 1/2` external, `Rc 1/2` internal) or `npt` (ASME B1.20.1 taper, `1/2-14 NPT`). An
|
||||
//! older design has none of them and needs no rewrite. The last release refuses a design saved
|
||||
//! with this (the newer format number).
|
||||
//! parallel internal thread, `Rp 1/2`), `bspt` (ISO 7-1 taper, `R 1/2` external, `Rc 1/2`
|
||||
//! internal) or `npt` (ASME B1.20.1 taper, `1/2-14 NPT`), and a tapped hole's `thread` may be
|
||||
//! `Rc 1/2` or `1/2-14 NPT`. An older design has none of them and needs no rewrite. The last
|
||||
//! release refuses a design saved with this (the newer format number).
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! A modelled thread saves how its ends are finished: `ease_out`, the runout at the inner end,
|
||||
//! away from where it starts (`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 these were being made are
|
||||
//! ISO 4753), with `chamfer_angle` and `chamfer_depth`. A tapped hole saves `modeled` (its thread
|
||||
//! cut into the wall) and that thread's `ease_out` towards the bottom. Without them, as in an
|
||||
//! older design, the groove stops square, no end is chamfered and a tapped hole's thread is only
|
||||
//! listed. Designs saved while these were being made are
|
||||
//! rewritten as they open: `tip: chamfer` (every free end) becomes `both`, and an `ease_in` (a
|
||||
//! runout at the start, which now always runs off its free end) is dropped. The last release
|
||||
//! refuses a design saved with this (the newer format number).
|
||||
|
||||
@@ -8,6 +8,10 @@ pub enum TappedThreadFamily {
|
||||
Unf,
|
||||
G,
|
||||
Rp,
|
||||
/// ISO 7-1's internal taper thread (`Rc 1/2`).
|
||||
Rc,
|
||||
/// ASME B1.20.1's taper pipe thread, internal (`1/2-14 NPT`).
|
||||
Npt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -22,7 +26,7 @@ pub struct TappedThread {
|
||||
impl TappedThread {
|
||||
pub fn profile_degrees(&self) -> f64 {
|
||||
match self.family {
|
||||
TappedThreadFamily::G | TappedThreadFamily::Rp => 55.0,
|
||||
TappedThreadFamily::G | TappedThreadFamily::Rp | TappedThreadFamily::Rc => 55.0,
|
||||
_ => 60.0,
|
||||
}
|
||||
}
|
||||
@@ -42,6 +46,24 @@ fn pipe_size(size: crate::pipe_thread_catalog::PipeThread) -> TappedThread {
|
||||
}
|
||||
}
|
||||
|
||||
/// An internal taper thread (Rc, NPT) of the size tables a modelled thread cuts
|
||||
/// ([`crate::pipe_thread_sizes`]). Its drill is the internal minor diameter where the thread
|
||||
/// ends, the reference plane's d - 2h at the face less the taper over the thread's length (ISO
|
||||
/// 7-1's useful thread, NPT's effective thread L2): derived from the standards' dimensions, so
|
||||
/// the tap cuts full form over the whole length. It is not a tap maker's recommendation.
|
||||
fn taper_size(family: TappedThreadFamily, size: crate::pipe_thread_sizes::PipeSize) -> TappedThread {
|
||||
use crate::thread_forms::ThreadForm;
|
||||
let p = size.pitch();
|
||||
let (form, h) = match family {
|
||||
TappedThreadFamily::Npt => (ThreadForm::Npt, crate::pipe_thread_sizes::NPT_HEIGHT * p),
|
||||
_ => (ThreadForm::Bspt, 0.640327 * p),
|
||||
};
|
||||
let designation = form.format(size.major, p, p);
|
||||
let designation = if form == ThreadForm::Bspt { designation.replacen("R ", "Rc ", 1) } else { designation };
|
||||
let drill = size.major - 2.0 * h - size.length / 16.0;
|
||||
TappedThread { family, designation, major_diameter: size.major, pitch: p, tap_drill: (drill * 1000.0).floor() / 1000.0 }
|
||||
}
|
||||
|
||||
// Diameter/pitch pairs verified against Guhring, plus Yamawa for M12x1.25.
|
||||
const METRIC_FINE: &[(f64, f64)] = &[
|
||||
(3.0, 0.35),
|
||||
@@ -110,6 +132,11 @@ pub fn tapped_thread_sizes(family: TappedThreadFamily) -> Vec<TappedThread> {
|
||||
if matches!(family, TappedThreadFamily::G | TappedThreadFamily::Rp) {
|
||||
return crate::pipe_thread_catalog::pipe_thread_sizes().map(pipe_size).filter(|s| s.family == family).collect();
|
||||
}
|
||||
match family {
|
||||
TappedThreadFamily::Rc => return crate::pipe_thread_sizes::iso_7_1().map(|z| taper_size(family, z)).collect(),
|
||||
TappedThreadFamily::Npt => return crate::pipe_thread_sizes::npt().map(|z| taper_size(family, z)).collect(),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if family == TappedThreadFamily::Metric {
|
||||
let mut sizes: Vec<_> = crate::metric_thread_sizes().iter().chain(METRIC_FINE).map(|&(d, p)| metric_size(d, p)).collect();
|
||||
@@ -131,7 +158,8 @@ pub fn tapped_thread_sizes(family: TappedThreadFamily) -> Vec<TappedThread> {
|
||||
}
|
||||
|
||||
/// Explicit metric custom sizes remain supported. Unified sizes must be in the verified
|
||||
/// catalogue and include their UNC/UNF suffix. G/Rp use explicit process drills; modeled Thread stays metric-only.
|
||||
/// catalogue and include their UNC/UNF suffix. G/Rp use explicit process drills, Rc/NPT the drill
|
||||
/// derived from their standards ([`taper_size`]).
|
||||
pub fn parse_tapped_thread(text: &str) -> Option<TappedThread> {
|
||||
if text.len() > 128 {
|
||||
return None;
|
||||
@@ -142,6 +170,18 @@ pub fn parse_tapped_thread(text: &str) -> Option<TappedThread> {
|
||||
if let Some(size) = crate::pipe_thread_catalog::parse_pipe_thread(text) {
|
||||
return Some(pipe_size(size));
|
||||
}
|
||||
// An internal taper thread: Rc or NPT (an external R is no hole's).
|
||||
use crate::thread_forms::ThreadForm;
|
||||
let taper = match ThreadForm::of_designation(text) {
|
||||
Some(ThreadForm::Bspt) if ThreadForm::pipe_internal(text) => Some((TappedThreadFamily::Rc, ThreadForm::Bspt)),
|
||||
Some(ThreadForm::Bspt) => return None,
|
||||
Some(ThreadForm::Npt) => Some((TappedThreadFamily::Npt, ThreadForm::Npt)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((family, form)) = taper {
|
||||
let size = form.parse(text, 1).ok()?;
|
||||
return tapped_thread_sizes(family).into_iter().find(|s| (s.major_diameter - size.major).abs() < 1e-9);
|
||||
}
|
||||
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_uppercase();
|
||||
let &(name, family, major_diameter, tpi, tap_drill) = UNIFIED.iter().find(|row| row.0 == normalized)?;
|
||||
Some(TappedThread { family, designation: name.into(), major_diameter, pitch: 25.4 / f64::from(tpi), tap_drill })
|
||||
@@ -163,14 +203,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rc and NPT holes: the size tables' diameters, the internal minor where the thread ends for
|
||||
/// the drill (Rc 1/2: d1 18.631 at the face, 13.2 mm of useful thread at 1 in 16; 1/2 NPT:
|
||||
/// E1 - h = 0.77843 - 0.05714 in at the face, L2 = 0.5337 in), and no external R.
|
||||
#[test]
|
||||
fn internal_taper_threads_drill_to_their_minor_where_the_thread_ends() {
|
||||
let rc = parse_tapped_thread("Rc1/2").unwrap();
|
||||
assert_eq!((rc.family, rc.designation.as_str(), rc.major_diameter), (TappedThreadFamily::Rc, "Rc 1/2", 20.955));
|
||||
assert!((rc.tap_drill - (18.631 - 13.2 / 16.0)).abs() < 2e-3, "{}", rc.tap_drill);
|
||||
let npt = parse_tapped_thread("1/2 NPT").unwrap();
|
||||
assert_eq!((npt.family, npt.designation.as_str()), (TappedThreadFamily::Npt, "1/2-14 NPT"));
|
||||
assert!((npt.tap_drill - (0.77843 - 0.8 / 14.0 - 0.5337 / 16.0) * 25.4).abs() < 2e-3, "{}", npt.tap_drill);
|
||||
assert_eq!(rc.profile_degrees(), 55.0);
|
||||
assert_eq!(npt.profile_degrees(), 60.0);
|
||||
assert!(parse_tapped_thread("R 1/2").is_none(), "an external taper thread is no hole's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tapped_catalogue_has_unique_valid_sizes_and_round_trips() {
|
||||
let mut names = std::collections::HashSet::new();
|
||||
for family in [TappedThreadFamily::Metric, TappedThreadFamily::Unc, TappedThreadFamily::Unf, TappedThreadFamily::G, TappedThreadFamily::Rp] {
|
||||
for family in [
|
||||
TappedThreadFamily::Metric,
|
||||
TappedThreadFamily::Unc,
|
||||
TappedThreadFamily::Unf,
|
||||
TappedThreadFamily::G,
|
||||
TappedThreadFamily::Rp,
|
||||
TappedThreadFamily::Rc,
|
||||
TappedThreadFamily::Npt,
|
||||
] {
|
||||
let sizes = tapped_thread_sizes(family);
|
||||
if family != TappedThreadFamily::Metric {
|
||||
assert_eq!(sizes.len(), if matches!(family, TappedThreadFamily::G | TappedThreadFamily::Rp) { 6 } else { 14 });
|
||||
}
|
||||
let count = match family {
|
||||
TappedThreadFamily::Metric => sizes.len(),
|
||||
TappedThreadFamily::G | TappedThreadFamily::Rp => 6,
|
||||
TappedThreadFamily::Rc => 15,
|
||||
TappedThreadFamily::Npt => crate::pipe_thread_sizes::npt().count(),
|
||||
_ => 14,
|
||||
};
|
||||
assert_eq!(sizes.len(), count, "{family:?}");
|
||||
for size in sizes {
|
||||
assert!(names.insert(size.designation.clone()));
|
||||
assert_eq!(parse_tapped_thread(&size.designation), Some(size.clone()));
|
||||
@@ -200,7 +269,7 @@ mod tests {
|
||||
let size = parse_tapped_thread(name).unwrap();
|
||||
assert_eq!((size.major_diameter, size.pitch, size.tap_drill), (d, p, drill));
|
||||
}
|
||||
for invalid in ["1/4-20 UNF", "#10-32 UNC", "1/4-19 BSP", "Rc1/4", "R1/4", "1/4-18 NPT", "1/4-20", "M8xNaN", "M8x8"] {
|
||||
for invalid in ["1/4-20 UNF", "#10-32 UNC", "1/4-19 BSP", "Rc 7/8", "R1/4", "1/4-27 NPT", "1/4-20", "M8xNaN", "M8x8"] {
|
||||
assert!(parse_tapped_thread(invalid).is_none(), "{invalid}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("hole")
|
||||
.key("H")
|
||||
.params("position: [x,y,z] on a face (numbers, or length expressions the hole follows) | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); flip?: bool (sketch points: drill to the other side); single?: bool (with one sketch point: a Single hole that follows the point, drilled into the face there); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points unless single); diameter | thread (a tapped hole: metric major minus pitch or tabulated UNC/UNF, G or Rp cut-tap drill) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); references?: [{edge: [x,y,z] on a straight edge of the face, distance: expr}] (at most two; the centre goes that far from each edge's line, in the face's plane, and follows the edges; with a numeric position only); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" or \"1/4-20 UNC\" (cosmetic); hand?: right|left (the tapped thread's hand; default right; needs thread); thread_depth?: positive length independent of drill depth (default: full useful cylindrical wall; must fit that wall); targets?: [body] (the bodies to drill; holes at the points of a sketch on a body's face default to that body, found along the holes' direction on it alone; explicit targets or participants override, [\"*\"] every body)"),
|
||||
.params("position: [x,y,z] on a face (numbers, or length expressions the hole follows) | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); flip?: bool (sketch points: drill to the other side); single?: bool (with one sketch point: a Single hole that follows the point, drilled into the face there); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points unless single); diameter | thread (a tapped hole: metric major minus pitch or tabulated UNC/UNF, G or Rp cut-tap drill, or the Rc/NPT drill to the internal minor diameter where the thread ends) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); references?: [{edge: [x,y,z] on a straight edge of the face, distance: expr}] (at most two; the centre goes that far from each edge's line, in the face's plane, and follows the edges; with a numeric position only); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\", \"1/4-20 UNC\", \"G 1/4\", \"Rp 1/4\", \"Rc 1/4\" or \"1/4-18 NPT\" (cosmetic unless modeled); hand?: right|left (the tapped thread's hand; default right; needs thread); thread_depth?: positive length independent of drill depth (default: full useful cylindrical wall; must fit that wall); modeled?: bool (needs thread: the tapped thread cut into the wall, the groove between its teeth, as solid.thread's modelled one; default false: listed only); ease_out?: expr|standard|short (modelled tapped threads: the inner end, towards the bottom, fades out over this length, ISO 3508's runout, standard 2.5 P, short 1.25 P; without it the groove stops square at thread_depth, or clear of a blind hole's bottom); targets?: [body] (the bodies to drill; holes at the points of a sketch on a body's face default to that body, found along the holes' direction on it alone; explicit targets or participants override, [\"*\"] every body)"),
|
||||
CommandSpec::new("solid.pipe", "Pipe", pipe)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("pipe")
|
||||
@@ -2434,13 +2434,21 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
return Err(bad(
|
||||
cmd,
|
||||
fine_only_hint(t).unwrap_or_else(|| format!("unknown tapped thread `{t}` (ISO metric or a catalogued UNC/UNF, G or Rp size)")),
|
||||
fine_only_hint(t).unwrap_or_else(|| format!("unknown tapped thread `{t}` (ISO metric or a catalogued UNC/UNF, G, Rp, Rc or NPT size)")),
|
||||
));
|
||||
}
|
||||
let thread_depth = match p.get("thread_depth") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(_) => Some(req_expr(cmd, p, "thread_depth")?),
|
||||
};
|
||||
let modeled = p.get("modeled").and_then(Value::as_bool).unwrap_or(false);
|
||||
if modeled && thread.is_none() {
|
||||
return Err(bad(cmd, "`modeled` needs `thread` (a tapped hole)"));
|
||||
}
|
||||
let ease_out = runout_param(s, p, cmd, "ease_out")?;
|
||||
if ease_out.is_some() && !modeled {
|
||||
return Err(bad(cmd, "`ease_out` is cut only on a modelled thread (`modeled: true`)"));
|
||||
}
|
||||
if let Some(d) = &thread_depth {
|
||||
if thread.is_none() {
|
||||
return Err(bad(cmd, "`thread_depth` requires `thread` (a tapped hole)"));
|
||||
@@ -2602,12 +2610,30 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
thread,
|
||||
thread_depth,
|
||||
hand,
|
||||
modeled,
|
||||
ease_out,
|
||||
to: to.map(FacePick),
|
||||
references,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// A thread's runout parameter: a length, `standard` (ISO 3508's 2.5 P) or `short` (1.25 P);
|
||||
/// none or 0: no runout.
|
||||
fn runout_param(s: &Session, p: &Value, cmd: &str, key: &str) -> Result<Option<solidcraft_doc::ThreadRunout>> {
|
||||
Ok(match p.get(key) {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(t)) if t == "standard" => Some(solidcraft_doc::ThreadRunout::Standard),
|
||||
Some(Value::String(t)) if t == "short" => Some(solidcraft_doc::ThreadRunout::Short),
|
||||
Some(Value::Number(n)) if n.as_f64() == Some(0.0) => None,
|
||||
Some(_) => {
|
||||
let e = expr(p, key).ok_or_else(|| bad(cmd, format!("`{key}` must be a length, \"standard\" or \"short\"")))?;
|
||||
check_expr(s, &e, Kind::Length, cmd, key)?;
|
||||
Some(solidcraft_doc::ThreadRunout::Length { length: e })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A hole's reference edges (#37): `references: [{edge: [x, y, z], distance: expr}]`, at most
|
||||
/// two, each distance a length.
|
||||
fn hole_reference_params(s: &Session, p: &Value, cmd: &str) -> Result<Vec<solidcraft_doc::HoleReference>> {
|
||||
@@ -2964,26 +2990,13 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if tip == ThreadTip::None && (chamfer_angle.is_some() || chamfer_depth.is_some()) {
|
||||
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) {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(t)) if t == "standard" => Some(solidcraft_doc::ThreadRunout::Standard),
|
||||
Some(Value::String(t)) if t == "short" => Some(solidcraft_doc::ThreadRunout::Short),
|
||||
Some(Value::Number(n)) if n.as_f64() == Some(0.0) => None,
|
||||
Some(_) => {
|
||||
let e = expr(p, key).ok_or_else(|| bad(cmd, format!("`{key}` must be a length, \"standard\" or \"short\"")))?;
|
||||
check_expr(s, &e, Kind::Length, cmd, key)?;
|
||||
Some(solidcraft_doc::ThreadRunout::Length { length: e })
|
||||
}
|
||||
})
|
||||
};
|
||||
if p.get("ease_in").is_some() {
|
||||
return Err(bad(
|
||||
cmd,
|
||||
"`ease_in` is gone: a thread's start runs off its free end, plain or chamfered (`tip: start`); fade the inner end with `ease_out`",
|
||||
));
|
||||
}
|
||||
let ease_out = ease("ease_out")?;
|
||||
let ease_out = runout_param(s, p, cmd, "ease_out")?;
|
||||
let length = expr(p, "length");
|
||||
if let Some(l) = &length {
|
||||
check_expr(s, l, Kind::Length, cmd, "length")?;
|
||||
|
||||
@@ -1212,3 +1212,47 @@ fn a_taper_pipe_thread_starts_at_a_free_end_and_is_named_for_its_side() {
|
||||
let (chamfered, threaded) = (exact_volume(&s, "Part"), exact_volume(&plain, "Part"));
|
||||
assert!(chamfered < threaded - 1e-3, "the tip cuts the first teeth: {chamfered} vs {threaded}");
|
||||
}
|
||||
|
||||
/// A tapped hole's thread cut into its wall (`modeled`), as the thread feature cuts one: an M8
|
||||
/// blind hole whose groove stops clear of the drill point's bottom, or fades out before it
|
||||
/// (`ease_out`, ISO 3508), which leaves more of the wall than the full form there; an Rc 1/2
|
||||
/// through a plate, drilled to its minor diameter where the thread ends and cut to its taper;
|
||||
/// each a valid body that reopens the same.
|
||||
#[test]
|
||||
fn a_modelled_tapped_hole_stops_clear_of_its_bottom_or_fades_out_before_it() {
|
||||
let blind = |extra: Value| {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":30,"width":30,"height":20,"body_name":"Plate"}));
|
||||
let mut p = json!({"position":[15,15,20],"direction":[0,0,-1],"thread":"M8","depth":14,"type":"drilled"});
|
||||
for (k, v) in extra.as_object().unwrap() {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
run(&mut s, "solid.hole", p);
|
||||
assert!(s.model.state().body("Plate").unwrap().body.validity().is_empty(), "{extra}");
|
||||
s
|
||||
};
|
||||
let (drilled, square, fade) = (blind(json!({})), blind(json!({"modeled":true})), blind(json!({"modeled":true,"ease_out":"standard"})));
|
||||
let (v0, v1, v2) = (exact_volume(&drilled, "Plate"), exact_volume(&square, "Plate"), exact_volume(&fade, "Plate"));
|
||||
assert!(v1 < v2 && v2 < v0, "drilled {v0}, square {v1}, fade {v2}");
|
||||
// The groove never reaches the drill point: the bottom's cone is the drilled one.
|
||||
assert!(square.model.state().threads.is_empty(), "a modelled thread is cut, not listed");
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&fade.doc.to_json()).unwrap());
|
||||
assert!((exact_volume(&reopened, "Plate") - v2).abs() < 1e-6);
|
||||
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":40,"width":40,"height":12,"body_name":"Plate"}));
|
||||
let out = run(&mut s, "solid.hole", json!({"position":[20,20,12],"direction":[0,0,-1],"thread":"Rc 1/2"}));
|
||||
let drilled = exact_volume(&s, "Plate");
|
||||
// d1 at the face less 13.2 mm of useful thread at 1 in 16, to the micrometre below.
|
||||
let drill = ((20.955_f64 - 2.0 * 0.640327 * 25.4 / 14.0 - 13.2 / 16.0) * 1000.0).floor() / 1000.0;
|
||||
assert!((drilled - (40.0 * 40.0 * 12.0 - PI * (drill / 2.0).powi(2) * 12.0)).abs() < 1e-3, "{drilled}");
|
||||
let id = out["feature"].as_u64().unwrap();
|
||||
let redefine = json!({"position":[20,20,12],"direction":[0,0,-1],"thread":"Rc 1/2","modeled":true});
|
||||
run(&mut s, "timeline.redefine", json!({"feature":id,"command":"solid.hole","params":redefine}));
|
||||
assert!(s.model.state().body("Plate").unwrap().body.validity().is_empty());
|
||||
assert!(exact_volume(&s, "Plate") < drilled - 1.0);
|
||||
|
||||
let err = |p: Value| Session::default().execute("solid.hole", &p).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(err(json!({"position":[0,0,0],"diameter":5,"modeled":true})).contains("`modeled` needs `thread`"));
|
||||
assert!(err(json!({"position":[0,0,0],"thread":"M8","ease_out":"standard"})).contains("modelled thread"));
|
||||
}
|
||||
|
||||
@@ -420,6 +420,10 @@ pub struct HoleOpts {
|
||||
pub thread_depth: String,
|
||||
/// The tapped thread's hand (right or left).
|
||||
pub hand: solidcraft_engine::doc::thread_forms::ThreadHand,
|
||||
/// The tapped thread cut into the wall, not only listed.
|
||||
pub modeled: bool,
|
||||
/// A modelled tapped thread's inner end, towards the bottom: square or a fade.
|
||||
pub ease_out: crate::dialogs_thread::Ease,
|
||||
}
|
||||
|
||||
impl Default for HoleOpts {
|
||||
@@ -436,6 +440,8 @@ impl Default for HoleOpts {
|
||||
thread_full_depth: true,
|
||||
thread_depth: "10 mm".into(),
|
||||
hand: solidcraft_engine::doc::thread_forms::ThreadHand::Right,
|
||||
modeled: false,
|
||||
ease_out: crate::dialogs_thread::Ease::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4487,7 +4493,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
return Err(error.clone());
|
||||
}
|
||||
solidcraft_engine::doc::parse_tapped_thread(&opts.thread)
|
||||
.ok_or("select a valid metric or catalogued UNC/UNF, G or Rp thread size")?;
|
||||
.ok_or("select a valid metric or catalogued UNC/UNF, G, Rp, Rc or NPT thread size")?;
|
||||
params["thread"] = json!(opts.thread.trim());
|
||||
if !opts.thread_full_depth {
|
||||
params["thread_depth"] = json!(opts.thread_depth.trim());
|
||||
@@ -4495,6 +4501,12 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
if opts.hand != solidcraft_engine::doc::thread_forms::ThreadHand::Right {
|
||||
params["hand"] = json!(opts.hand);
|
||||
}
|
||||
if opts.modeled {
|
||||
params["modeled"] = json!(true);
|
||||
if let Some(e) = opts.ease_out.param() {
|
||||
params["ease_out"] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
match *kind {
|
||||
1 => {
|
||||
@@ -5416,7 +5428,22 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
d.extra.insert("pull".into(), pt3(pull.0));
|
||||
d
|
||||
}
|
||||
FeatureKind::Hole { position, at, direction, diameter, depth, hole, points, thread, thread_depth, hand, to, references } => {
|
||||
FeatureKind::Hole {
|
||||
position,
|
||||
at,
|
||||
direction,
|
||||
diameter,
|
||||
depth,
|
||||
hole,
|
||||
points,
|
||||
thread,
|
||||
thread_depth,
|
||||
hand,
|
||||
modeled,
|
||||
ease_out,
|
||||
to,
|
||||
references,
|
||||
} => {
|
||||
let mut d = start("solid.hole")?;
|
||||
let mut k = hole_defaults(s);
|
||||
if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = &mut k {
|
||||
@@ -5428,6 +5455,8 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
opts.tapped = true;
|
||||
opts.thread = th.clone();
|
||||
opts.hand = *hand;
|
||||
opts.modeled = *modeled;
|
||||
opts.ease_out = crate::dialogs_thread::Ease::of(ease_out);
|
||||
}
|
||||
opts.thread_full_depth = thread_depth.is_none();
|
||||
if let Some(depth) = thread_depth {
|
||||
|
||||
@@ -568,7 +568,11 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
|
||||
if opts.tapped {
|
||||
enter |= crate::dialogs_thread::hole_rows(ui, &app.session.doc, &mut opts.thread, &mut opts.custom_thread);
|
||||
crate::dialogs_thread::hole_hand_row(ui, &mut opts.hand);
|
||||
ui_kit::checkbox_grid(ui, &mut [("Full thread depth", &mut opts.thread_full_depth)], 1);
|
||||
ui_kit::checkbox_grid(ui, &mut [("Full thread depth", &mut opts.thread_full_depth), ("Modeled", &mut opts.modeled)], 1);
|
||||
// A modelled thread's inner end, towards the bottom, as the thread dialog's.
|
||||
if opts.modeled {
|
||||
enter |= crate::dialogs_thread::end_rows(ui, &mut opts.ease_out);
|
||||
}
|
||||
}
|
||||
// Through all, the tip lies past the part.
|
||||
if !opts.all {
|
||||
|
||||
@@ -70,11 +70,19 @@ impl HoleThreadDraft {
|
||||
/// Custom remains a dialog mode even when its current values match a catalogue size.
|
||||
pub(crate) fn hole_rows(ui: &mut egui::Ui, doc: &Document, designation: &mut String, custom: &mut Option<HoleThreadDraft>) -> bool {
|
||||
let designation_id = ui.make_persistent_id("hole_thread_designation");
|
||||
let families = [TappedThreadFamily::Metric, TappedThreadFamily::Unc, TappedThreadFamily::Unf, TappedThreadFamily::G, TappedThreadFamily::Rp];
|
||||
let families = [
|
||||
TappedThreadFamily::Metric,
|
||||
TappedThreadFamily::Unc,
|
||||
TappedThreadFamily::Unf,
|
||||
TappedThreadFamily::G,
|
||||
TappedThreadFamily::Rp,
|
||||
TappedThreadFamily::Rc,
|
||||
TappedThreadFamily::Npt,
|
||||
];
|
||||
let family = parse_tapped_thread(designation).map(|s| s.family).unwrap_or(TappedThreadFamily::Metric);
|
||||
let mut profile = families.iter().position(|f| *f == family).unwrap_or(0);
|
||||
let before_profile = profile;
|
||||
form_row(ui, "Profile", |ui| combo(ui, "hole_thread_profile", &["ISO metric", "UNC", "UNF", "G / BSPP", "Rp"], &mut profile));
|
||||
form_row(ui, "Profile", |ui| combo(ui, "hole_thread_profile", &["ISO metric", "UNC", "UNF", "G / BSPP", "Rp", "Rc", "NPT"], &mut profile));
|
||||
let family = families.get(profile).copied().unwrap_or(TappedThreadFamily::Metric);
|
||||
let sizes = tapped_thread_sizes(family);
|
||||
if family != TappedThreadFamily::Metric {
|
||||
@@ -107,6 +115,8 @@ pub(crate) fn hole_rows(ui: &mut egui::Ui, doc: &Document, designation: &mut Str
|
||||
TappedThreadFamily::Unf => "hole_thread_catalogue_unf",
|
||||
TappedThreadFamily::G => "hole_thread_catalogue_g",
|
||||
TappedThreadFamily::Rp => "hole_thread_catalogue_rp",
|
||||
TappedThreadFamily::Rc => "hole_thread_catalogue_rc",
|
||||
TappedThreadFamily::Npt => "hole_thread_catalogue_npt",
|
||||
};
|
||||
combo(ui, catalogue_id, &labels, &mut selected);
|
||||
if selected != before {
|
||||
@@ -118,10 +128,10 @@ pub(crate) fn hole_rows(ui: &mut egui::Ui, doc: &Document, designation: &mut Str
|
||||
}
|
||||
}
|
||||
});
|
||||
if matches!(family, TappedThreadFamily::G | TappedThreadFamily::Rp) {
|
||||
if matches!(family, TappedThreadFamily::G | TappedThreadFamily::Rp | TappedThreadFamily::Rc | TappedThreadFamily::Npt) {
|
||||
form_row(ui, "Thread diameter", |ui| {
|
||||
if let Some(size) = parse_tapped_thread(designation) {
|
||||
ui.label(format!("{:.3} mm, 55°", size.major_diameter));
|
||||
ui.label(format!("{:.3} mm, {}°", size.major_diameter, size.profile_degrees()));
|
||||
}
|
||||
});
|
||||
form_row(ui, "Pitch", |ui| {
|
||||
@@ -205,7 +215,7 @@ pub struct Ease {
|
||||
}
|
||||
|
||||
impl Ease {
|
||||
fn of(r: &Option<ThreadRunout>) -> Ease {
|
||||
pub(crate) fn of(r: &Option<ThreadRunout>) -> Ease {
|
||||
match r {
|
||||
None => Ease::default(),
|
||||
Some(ThreadRunout::Standard) => Ease { fade: true, ..Ease::default() },
|
||||
@@ -215,7 +225,7 @@ impl Ease {
|
||||
}
|
||||
|
||||
/// The command's `ease_out`, none for a square end.
|
||||
fn param(&self) -> Option<Value> {
|
||||
pub(crate) fn param(&self) -> Option<Value> {
|
||||
let typed = self.length.trim();
|
||||
self.fade.then(|| match (typed.is_empty(), self.short) {
|
||||
(false, _) => json!(typed),
|
||||
@@ -446,18 +456,24 @@ pub(crate) fn rows(
|
||||
let depth = if f == ThreadForm::Metric { "ui.iso_4753" } else { "thread.chamfer_depth_needed" };
|
||||
enter |= hint_row(ui, "Chamfer Depth", &mut o.chamfer_depth, tr(depth));
|
||||
}
|
||||
// The inner end: square, or a fade over its length.
|
||||
let mut fade = usize::from(o.ease_out.fade);
|
||||
choice_row(ui, "End", "thread_end", &mut fade, ENDS_AT);
|
||||
o.ease_out.fade = fade == 1;
|
||||
if o.ease_out.fade {
|
||||
let hint = if o.ease_out.short { tr("thread.fade_short") } else { tr("thread.fade_standard") };
|
||||
enter |= hint_row(ui, "Fade Length", &mut o.ease_out.length, hint);
|
||||
}
|
||||
enter |= end_rows(ui, &mut o.ease_out);
|
||||
});
|
||||
(enter, reset)
|
||||
}
|
||||
|
||||
/// The inner end of a modelled thread (a thread's, or a tapped hole's towards its bottom):
|
||||
/// square, or a fade over its length.
|
||||
pub(crate) fn end_rows(ui: &mut egui::Ui, ease: &mut Ease) -> bool {
|
||||
let mut fade = usize::from(ease.fade);
|
||||
choice_row(ui, "End", "thread_end", &mut fade, ENDS_AT);
|
||||
ease.fade = fade == 1;
|
||||
if !ease.fade {
|
||||
return false;
|
||||
}
|
||||
let hint = if ease.short { tr("thread.fade_short") } else { tr("thread.fade_standard") };
|
||||
hint_row(ui, "Fade Length", &mut ease.length, hint)
|
||||
}
|
||||
|
||||
/// The Size choice of a form with a size table, and the Pitch choice of the metric ones (an
|
||||
/// inch designation names its threads per inch).
|
||||
fn size_rows(ui: &mut egui::Ui, f: ThreadForm, designation: &mut String, suggested: &mut Option<String>) {
|
||||
@@ -766,6 +782,37 @@ mod contribution_tests {
|
||||
assert_eq!((h.app.session.undo.len(), h.app.session.redo.len()), history);
|
||||
}
|
||||
|
||||
/// A tapped hole offers what a thread does: Modeled, and then its inner end, square or a
|
||||
/// fade, towards the bottom. OK cuts it; editing the hole brings them back.
|
||||
#[test]
|
||||
fn a_tapped_hole_is_modelled_with_the_threads_end() {
|
||||
let (app, dialog) = hole();
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app = app;
|
||||
h.app.dialog = Some(dialog);
|
||||
h.frames(3);
|
||||
for handle in ["dialog:Hole Tap Type", "dialog:Hole Tap Type:Tapped"] {
|
||||
h.step(&json!({"click":{"handle":handle}})).unwrap();
|
||||
}
|
||||
h.frames(2);
|
||||
assert!(crate::scenario::rect_of("dialog:row:End").is_none(), "a listed thread has no end to finish");
|
||||
h.step(&json!({"click":{"handle":"dialog:Modeled"}})).unwrap();
|
||||
h.frames(2);
|
||||
for handle in ["dialog:End", "dialog:End:ui.fade"] {
|
||||
h.step(&json!({"click":{"handle":handle}})).unwrap();
|
||||
}
|
||||
h.frames(2);
|
||||
let (cmd, p) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
|
||||
assert_eq!((&p["modeled"], &p["ease_out"]), (&json!(true), &json!("standard")), "{p}");
|
||||
let id = h.app.run(&cmd, p.clone()).unwrap()["feature"].as_u64().unwrap();
|
||||
h.app.dialog = None;
|
||||
h.app.edit_feature(id);
|
||||
h.frames(2);
|
||||
let (_, again) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
|
||||
let again = if again.get("params").is_some() { again["params"].clone() } else { again };
|
||||
assert_eq!((&again["modeled"], &again["ease_out"]), (&json!(true), &json!("standard")), "{again}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_designation_keeps_keyboard_focus_when_custom_fields_appear() {
|
||||
let (app, dialog) = hole();
|
||||
@@ -937,10 +984,10 @@ mod contribution_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expanded_catalogue_rejects_tapered_holes_and_nonmetric_thread_features() {
|
||||
fn expanded_catalogue_rejects_external_taper_holes_and_nonmetric_thread_features() {
|
||||
let (mut app, _) = hole();
|
||||
let before = app.session.doc.to_json();
|
||||
for name in ["1/4-18 NPT", "Rc1/4", "R1/4", "1/4-20 UNF"] {
|
||||
for name in ["R1/4", "1/4-20 UNF"] {
|
||||
assert!(app.run("solid.hole", json!({"position":[10,10,10],"thread":name})).is_err());
|
||||
assert_eq!(app.session.doc.to_json(), before);
|
||||
}
|
||||
|
||||
+26
-13
@@ -1,8 +1,9 @@
|
||||
# Tapped-hole catalogue
|
||||
|
||||
The Hole dialog offers ISO metric coarse sizes, 15 additional fine diameter/pitch
|
||||
pairs, a finite set of 14 UNC and 14 UNF sizes, and six sizes each for internal
|
||||
G/BSPP and Rp parallel pipe threads. This is a verified subset,
|
||||
pairs, a finite set of 14 UNC and 14 UNF sizes, six sizes each for internal
|
||||
G/BSPP and Rp parallel pipe threads, and the ISO 7-1 Rc and ASME B1.20.1 NPT
|
||||
internal taper threads (every size of their tables, `docs/threads.md`). This is a verified subset,
|
||||
not complete coverage of these standards. Unified sizes range from #4 through
|
||||
#12 and 1/4 through 1 inch. Include the family in a designation, for example
|
||||
`1/4-20 UNC` or `#10-32 UNF`.
|
||||
@@ -26,11 +27,23 @@ not presented as a tabulated 75% cut-tap recommendation. Existing files retain
|
||||
their saved diameter expressions. Custom metric dimensions remain editable, and
|
||||
Custom stays selected even when its current dimensions match a catalogue entry.
|
||||
|
||||
Tapped holes create the physical pilot bore and cosmetic thread metadata. The
|
||||
separate Thread command and its automatic sizing remain ISO metric only; this
|
||||
catalogue does not enable modelled Unified or pipe threads. Rc, external R, NPT,
|
||||
thread classes, handedness and sealing behavior are outside this tranche.
|
||||
No tapered thread is represented as a cylindrical hole.
|
||||
Tapped holes create the physical pilot bore and the thread, listed (cosmetic) or,
|
||||
with `modeled`, cut into the wall as the Thread command cuts a modelled thread:
|
||||
the entry is where it starts, and towards the bottom it stops square at the
|
||||
thread depth, clear of a blind hole's bottom, or fades out (`ease_out`, ISO 3508).
|
||||
A Unified thread is cut with the ISO 68-1 basic profile both share. External R,
|
||||
thread classes and sealing behaviour are outside this catalogue.
|
||||
|
||||
## Taper Rc and NPT threads
|
||||
|
||||
An Rc or NPT hole is drilled to the internal thread's minor diameter where the
|
||||
thread ends: the reference plane's minor (d - 2h for ISO 7-1, E1 - h for NPT) at
|
||||
the face, less the 1 in 16 taper over the thread's length (ISO 7-1's useful
|
||||
thread, NPT's effective thread L2). The drill is derived from the standards'
|
||||
dimensions so that the tap cuts full form over the whole length; it is not a tap
|
||||
maker's recommendation, and an explicit `diameter` overrides it. Listed, the hole
|
||||
is that cylindrical drill; modelled, the thread's taper is cut into it (the tap's
|
||||
minor cone and the helical groove).
|
||||
|
||||
## Parallel G/BSPP and Rp threads
|
||||
|
||||
@@ -38,8 +51,8 @@ Both families use a 55-degree profile and a cylindrical pilot bore. Keep G and R
|
||||
separate: their process-drill recommendations differ. The UI shows the nominal
|
||||
pipe designation, actual basic major diameter, pitch/TPI and pilot drill. The
|
||||
command accepts canonical labels such as `G 1/4` and `Rp 1/4`, compact forms
|
||||
`G1/4` and `Rp1/4`, and `BSPP 1/4` as an alias for G. The separate Thread command
|
||||
remains metric-only.
|
||||
`G1/4` and `Rp1/4`, and `BSPP 1/4` as an alias for G. The Thread command cuts
|
||||
them too (forms `bspp` and `rp`).
|
||||
|
||||
| Nominal | TPI | Major mm | Pitch diameter mm | Minor mm | G drill mm | Rp drill mm |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
@@ -54,8 +67,8 @@ Pitch is exactly 25.4/TPI. These basic thread dimensions are separate from proce
|
||||
bores. The catalogue uses Yamawa's drill column as its default pilot. Its separate
|
||||
recommended process-bore references, for the six rows above, are G: 8.78, 11.78,
|
||||
15.28, 19.04, 24.52, 30.77 mm and Rp: 8.55, 11.5, 14.9, 18.55, 24.1, 30.25 mm.
|
||||
A saved explicit diameter remains authoritative. The geometry stays a physical
|
||||
cylindrical pilot plus cosmetic designation, without a helical cut.
|
||||
A saved explicit diameter remains authoritative. Listed, the geometry is the
|
||||
cylindrical pilot plus the designation; modelled, the helical groove is cut too.
|
||||
|
||||
## Primary sources
|
||||
|
||||
@@ -79,8 +92,8 @@ cylindrical pilot plus cosmetic designation, without a helical cut.
|
||||
## Trial
|
||||
|
||||
On a small plate, open Hole, choose Tapped, then switch Profile between ISO metric,
|
||||
UNC, UNF, G / BSPP and Rp. Try M8, M8x1, 1/4-20 UNC, #10-32 UNF, G 1/4
|
||||
and Rp 1/4. Check the displayed drill,
|
||||
UNC, UNF, G / BSPP, Rp, Rc and NPT. Try M8, M8x1, 1/4-20 UNC, #10-32 UNF, G 1/4,
|
||||
Rp 1/4, Rc 1/4 and 1/4-18 NPT, listed and Modeled, with the End Square or Fade. Check the displayed drill,
|
||||
create the hole, save, reopen and check the designation and bore. In ISO metric,
|
||||
choose Custom, edit the nominal diameter and pitch, and verify that Custom stays
|
||||
selected and an invalid draft cannot apply an earlier valid value.
|
||||
|
||||
Reference in New Issue
Block a user