Tapped holes: a preference keeps metric's major-less-pitch drill

New metric tapped holes drill the tap maker's drill (M8 6.8 mm). The Design preference "Metric
tapped holes drill the major diameter less the pitch" (`metric_tap_drill_major_less_pitch`, off
by default) gives them the drill every metric size took before (M8 6.75 mm): the Hole dialog
shows and sends it, and `solid.hole` given a thread and no `diameter` drills it, through the
session as `auto_project` is (kept when a design is opened or started, and across tabs). Unified,
pipe and taper sizes keep their tabulated drills; a given diameter is unchanged. Saved designs
keep the drill they stored either way.

Labels in en, ar, he, ja, zh, de and ko.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
jyviko
2026-10-27 12:27:35 -04:00
committed by Irakli Kourtis
parent 05f1c6b9ac
commit 87e7fe472b
11 changed files with 104 additions and 7 deletions
+14
View File
@@ -1,35 +1,49 @@
{
"en": {
"prefs.holes": "Holes",
"prefs.metric_tap_drill_major_less_pitch": "Metric tapped holes drill the major diameter less the pitch (M8: 6.75 mm), not the tap maker's drill",
"thread.drill_range": "The tap maker's drill is {drill} mm. The internal thread's standard minor diameter is {minor} mm. A drill from {low} to {high} mm is accepted.",
"thread.drill_range_custom": "The drill is the major diameter less the pitch, {drill} mm. The internal thread's standard minor diameter is {minor} mm. A drill from {low} to {high} mm is accepted.",
"thread.drill_range_taper": "The tap maker's drill is {drill} mm. The standard's minor diameter where the thread ends is {minor} mm. A drill from {low} to {high} mm is accepted."
},
"ar": {
"prefs.holes": "الثقوب",
"prefs.metric_tap_drill_major_less_pitch": "تُثقب الثقوب الملولبة المترية بالقطر الأكبر ناقص الخطوة (M8: 6.75 مم) بدل مثقاب صانع اللقمة",
"thread.drill_range": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_custom": "المثقاب هو القطر الأكبر ناقص الخطوة، {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_taper": "مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم."
},
"he": {
"prefs.holes": "חורים",
"prefs.metric_tap_drill_major_less_pitch": "חורים מתוברגים מטריים נקדחים בקוטר הגדול פחות הפסיעה (M8: 6.75 מ\"מ) ולא במקדח של יצרן המברז",
"thread.drill_range": "מקדח יצרן המברז הוא {drill} מ\"מ. הקוטר הקטן התקני של התבריג הפנימי הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ.",
"thread.drill_range_custom": "המקדח הוא הקוטר הגדול פחות הפסיעה, {drill} מ\"מ. הקוטר הקטן התקני של התבריג הפנימי הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ.",
"thread.drill_range_taper": "מקדח יצרן המברז הוא {drill} מ\"מ. הקוטר הקטן התקני בסוף התבריג הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ."
},
"ja": {
"prefs.holes": "穴",
"prefs.metric_tap_drill_major_less_pitch": "メートルねじ穴の下穴を、タップメーカーの推奨値ではなく外径からピッチを引いた値にする(M8: 6.75 mm)",
"thread.drill_range": "タップメーカーの下穴ドリルは {drill} mm です。めねじの規格の内径は {minor} mm です。{low}~{high} mm のドリルを使用できます。",
"thread.drill_range_custom": "下穴ドリルは外径からピッチを引いた {drill} mm です。めねじの規格の内径は {minor} mm です。{low}~{high} mm のドリルを使用できます。",
"thread.drill_range_taper": "タップメーカーの下穴ドリルは {drill} mm です。ねじ終端での規格の内径は {minor} mm です。{low}~{high} mm のドリルを使用できます。"
},
"zh": {
"prefs.holes": "孔",
"prefs.metric_tap_drill_major_less_pitch": "公制螺纹孔的底孔取大径减螺距(M8:6.75 mm),而非丝锥制造商的钻头",
"thread.drill_range": "丝锥制造商的钻头为 {drill} mm。内螺纹的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。",
"thread.drill_range_custom": "钻头为大径减螺距,{drill} mm。内螺纹的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。",
"thread.drill_range_taper": "丝锥制造商的钻头为 {drill} mm。螺纹终端处的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。"
},
"de": {
"prefs.holes": "Bohrungen",
"prefs.metric_tap_drill_major_less_pitch": "Metrische Gewindebohrungen mit Nenndurchmesser minus Steigung bohren (M8: 6,75 mm), nicht mit dem Bohrer des Gewindebohrerherstellers",
"thread.drill_range": "Der Bohrer des Gewindebohrerherstellers ist {drill} mm. Der genormte Kerndurchmesser des Innengewindes ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen.",
"thread.drill_range_custom": "Der Bohrer ist der Nenndurchmesser minus Steigung, {drill} mm. Der genormte Kerndurchmesser des Innengewindes ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen.",
"thread.drill_range_taper": "Der Bohrer des Gewindebohrerherstellers ist {drill} mm. Der genormte Kerndurchmesser am Gewindeende ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen."
},
"ko": {
"prefs.holes": "구멍",
"prefs.metric_tap_drill_major_less_pitch": "미터 탭 구멍을 탭 제조사의 드릴 대신 바깥지름에서 피치를 뺀 값으로 뚫기 (M8: 6.75 mm)",
"thread.drill_range": "탭 제조사의 드릴은 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
"thread.drill_range_custom": "드릴은 바깥지름에서 피치를 뺀 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
"thread.drill_range_taper": "탭 제조사의 드릴은 {drill} mm입니다. 나사 끝에서의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다."
+10
View File
@@ -485,6 +485,16 @@ impl TappedThread {
(self.minor_diameter().min(self.tap_drill), self.tap_drill * (1.0 + DRILL_ALLOWANCE))
}
/// The drill a new hole of this size takes without a `diameter`: the tap maker's, or for an
/// ISO metric size with `major_less_pitch` (the preference that keeps the drill every metric
/// size took before) the major diameter less the pitch, to the micrometre.
pub fn default_drill(&self, major_less_pitch: bool) -> f64 {
match (self.family, major_less_pitch) {
(TappedThreadFamily::Metric, true) => ((self.major_diameter - self.pitch) * 1000.0).round() / 1000.0,
_ => self.tap_drill,
}
}
/// Whether a hole of `diameter` mm can be tapped to this size; the error names the range.
pub fn check_drill(&self, diameter: f64) -> Result<(), String> {
let (lo, hi) = self.drill_range();
+1 -1
View File
@@ -2545,7 +2545,7 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
}
(None, Some(t), None) => {
let size = solidcraft_doc::parse_tapped_thread(t).ok_or_else(|| bad(cmd, "thread"))?;
Expr::new(format!("{} mm", size.tap_drill))
Expr::new(format!("{} mm", size.default_drill(s.metric_tap_drill_major_less_pitch)))
}
(None, None, None) => return Err(bad(cmd, "needs `diameter`, `thread` (a tapped hole) or `clearance` (a bolt size)")),
};
+7
View File
@@ -368,6 +368,9 @@ pub struct Session {
pub(crate) followed: Option<cmd::inspect::Followed>,
/// Project a face's edges into a sketch created on it (the default; `sketch.auto_project`).
pub auto_project: bool,
/// A new metric tapped hole given no `diameter` drills the major diameter less the pitch,
/// as before the tap maker's drills (a user preference); false: the tap maker's drill.
pub metric_tap_drill_major_less_pitch: bool,
/// How calls that break a command's parameter contract are treated (off by default). It
/// belongs to the session, not the design, so it survives opening and starting designs.
pub param_check: schema::ParamCheck,
@@ -569,6 +572,7 @@ impl Session {
document_id: cmd::inspect::new_document_id(),
followed: None,
auto_project: true,
metric_tap_drill_major_less_pitch: false,
param_check: schema::ParamCheck::Off,
easyprint_upload: None,
live_links: None,
@@ -613,6 +617,7 @@ impl Session {
/// checkpoint keeps, not the replaced session's file path (docs/mcp.md).
pub fn replace_design(&mut self, fresh: Session) {
let auto_project = self.auto_project;
let metric_tap_drill_major_less_pitch = self.metric_tap_drill_major_less_pitch;
let param_check = self.param_check;
let easyprint_upload = self.easyprint_upload.take();
let render_viewport = self.render_viewport.clone();
@@ -626,6 +631,7 @@ impl Session {
*self = fresh;
self.crash_journal = crash_journal;
self.auto_project = auto_project;
self.metric_tap_drill_major_less_pitch = metric_tap_drill_major_less_pitch;
self.param_check = param_check;
self.easyprint_upload = easyprint_upload;
self.render_viewport = render_viewport;
@@ -1338,6 +1344,7 @@ impl Session {
document_id: self.document_id.clone(),
followed: None,
auto_project: self.auto_project,
metric_tap_drill_major_less_pitch: self.metric_tap_drill_major_less_pitch,
param_check: self.param_check,
easyprint_upload: self.easyprint_upload.clone(),
live_links: None,
+8 -1
View File
@@ -566,7 +566,14 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
opts.tapped = tap == 1;
});
if opts.tapped {
enter |= crate::dialogs_thread::hole_rows(ui, &app.session.doc, &mut opts.thread, &mut opts.custom_thread, &mut opts.drill);
enter |= crate::dialogs_thread::hole_rows(
ui,
&app.session.doc,
&mut opts.thread,
&mut opts.custom_thread,
&mut opts.drill,
app.preferences.metric_tap_drill_major_less_pitch,
);
crate::dialogs_thread::hole_hand_row(ui, &mut opts.hand);
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.
+7 -5
View File
@@ -74,6 +74,7 @@ pub(crate) fn hole_rows(
designation: &mut String,
custom: &mut Option<HoleThreadDraft>,
drill: &mut Option<String>,
major_less_pitch: bool,
) -> bool {
let before = designation.clone();
let designation_id = ui.make_persistent_id("hole_thread_designation");
@@ -169,12 +170,13 @@ pub(crate) fn hole_rows(
form_row(ui, "Tap drill diameter", |ui| {
match parse_tapped_thread(designation).filter(|_| custom.as_ref().is_none_or(|draft| draft.error.is_none())) {
Some(size) => {
let default = format!("{} mm", size.tap_drill);
let default = format!("{} mm", size.default_drill(major_less_pitch));
let (lo, hi) = size.drill_range();
let mm = |v: f64| format!("{:.3}", v);
let args = [("drill", mm(size.tap_drill)), ("minor", mm(size.minor_diameter())), ("low", mm(lo)), ("high", mm(hi))];
let args =
[("drill", mm(size.default_drill(major_less_pitch))), ("minor", mm(size.minor_diameter())), ("low", mm(lo)), ("high", mm(hi))];
let args: Vec<(&str, &str)> = args.iter().map(|(k, v)| (*k, v.as_str())).collect();
let id = match (size.taper_drill().is_some(), size.drill_is_tabulated()) {
let id = match (size.taper_drill().is_some(), size.drill_is_tabulated() && size.default_drill(major_less_pitch) == size.tap_drill) {
(true, _) => "thread.drill_range_taper",
(false, true) => "thread.drill_range",
(false, false) => "thread.drill_range_custom",
@@ -982,7 +984,7 @@ mod contribution_tests {
let doc = Document::default();
let _ = ctx.run_ui(Default::default(), |ui| {
egui::Grid::new("hole_catalogue").show(ui, |ui| {
hole_rows(ui, &doc, &mut designation, &mut custom, &mut None);
hole_rows(ui, &doc, &mut designation, &mut custom, &mut None, false);
});
});
assert_eq!(designation, original);
@@ -1131,7 +1133,7 @@ mod contribution_tests {
let doc = Document::default();
let _ = context.run_ui(Default::default(), |ui| {
egui::Grid::new("authored_hole").show(ui, |ui| {
hole_rows(ui, &doc, &mut hole, &mut custom, &mut None);
hole_rows(ui, &doc, &mut hole, &mut custom, &mut None, false);
});
});
assert_eq!(hole, original);
+1
View File
@@ -158,6 +158,7 @@ fn swap_in(app: &mut SolveApp, mut slot: DocSlot) -> DocSlot {
put_view(app, slot.view);
// Preferences the session holds go with the user, not the tab (#680).
app.session.auto_project = out.session.auto_project;
app.session.metric_tap_drill_major_less_pitch = out.session.metric_tap_drill_major_less_pitch;
// Views cache by revision: the incoming design must not look unchanged.
let before = app.session.revision;
app.session.invalidate_view(out_rev);
+2
View File
@@ -974,6 +974,8 @@ impl SolveApp {
if matches!(id, "file.save_mesh" | "file.send_to_slicer") {
solidcraft_engine::slicers::set_chosen_programs(&self.preferences.custom_slicers);
}
// The preference the hole command reads, current even before the next frame applies it.
self.session.metric_tap_drill_major_less_pitch = self.preferences.metric_tap_drill_major_less_pitch;
let t0 = now_ms();
let r = self.session.execute(id, &params).map_err(|e| e.to_string());
self.big_operation |= now_ms() - t0 >= recovery_ui::BIG_OPERATION_MS;
+46
View File
@@ -617,6 +617,10 @@ preference_settings! {
/// Up/Down and the wheel over a focused value field step its plain number (`value_step`,
/// #2320). Off by default: the keys move the text cursor and the wheel scrolls, as before.
step_values: bool = false, PAGE_DESIGN => check_id("ui.step_values");
/// New metric tapped holes drill the major diameter less the pitch (M8: 6.75 mm), as before
/// the tap maker's drills; off by default: the tap maker's drill (M8: 6.8 mm). Saved designs
/// keep the drill they stored either way.
metric_tap_drill_major_less_pitch: bool = false, PAGE_DESIGN => check_id("prefs.metric_tap_drill_major_less_pitch");
/// Reopen the saved designs that were open when the app was last closed (#5542).
reopen_designs: bool = true, PAGE_GENERAL;
/// Those designs' files, in tab order, as the last quit left them.
@@ -1102,6 +1106,8 @@ fn defaults(app: &mut SolveApp, page: usize) {
/// the interface scale.
pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
apply_network(&app.preferences);
// The hole command reads the metric tapped drill rule from the session.
app.session.metric_tap_drill_major_less_pitch = app.preferences.metric_tap_drill_major_less_pitch;
// The profiler records while the preference is on (switched when it changes; at start-up only
// when on, so SOLIDCRAFT_PROFILE=1 is not undone by the default).
let profile = app.preferences.profile_operations;
@@ -2422,6 +2428,46 @@ mod tests {
assert!(!solidcraft_engine::profile::enabled());
}
/// New metric tapped holes drill the tap maker's drill (M8: 6.8 mm); the preference keeps
/// the major diameter less the pitch they took before (6.75 mm). It reaches the hole command
/// whether set through the control channel or the Preferences page, and a hole given its
/// diameter keeps it either way.
#[test]
fn the_metric_tap_drill_preference_keeps_major_less_pitch() {
use serde_json::json;
let plate = || {
let mut h = crate::scenario::Harness::new();
h.app.run("solid.box", json!({"length":40,"width":20,"height":10,"body_name":"Plate"})).unwrap();
h
};
let drilled = |h: &mut crate::scenario::Harness, params: serde_json::Value| {
let id = h.app.run("solid.hole", params).unwrap()["feature"].as_u64().unwrap();
match &h.app.session.doc.feature(id).unwrap().kind {
solidcraft_engine::doc::FeatureKind::Hole { diameter, .. } => diameter.to_string(),
k => panic!("{k:?}"),
}
};
let m8 = json!({"position":[10,10,10],"thread":"M8"});
let mut h = plate();
h.frames(1);
assert!(!h.app.preferences.metric_tap_drill_major_less_pitch, "off by default");
assert_eq!(drilled(&mut h, m8.clone()), "6.8 mm");
let mut h = plate();
h.call("ui.prefs", json!({"metric_tap_drill_major_less_pitch": true}));
h.frames(2);
assert!(h.app.session.metric_tap_drill_major_less_pitch);
assert_eq!(drilled(&mut h, m8.clone()), "6.75 mm");
// Only metric changes: a Unified size keeps its tabulated drill.
assert_eq!(drilled(&mut h, json!({"position":[25,10,10],"thread":"1/4-20 UNC"})), "5.1054 mm");
let mut given = m8;
given["position"] = json!([33, 10, 10]);
given["diameter"] = json!("6.9 mm");
assert_eq!(drilled(&mut h, given), "6.9 mm");
// A design opened or started keeps the user's choice.
h.app.run("file.new", json!({})).unwrap();
assert!(h.app.session.metric_tap_drill_major_less_pitch);
}
#[test]
fn the_ghost_position_defaults_round_trips_and_reads_tolerantly() {
use super::SpaceMouseGhost::{LeftViewport as Current, LeftWindow as Left, RightViewport as Right};
+4
View File
@@ -36,6 +36,10 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let step = setting::step_values(app, ui).on_hover_text(tip);
crate::scenario::publish_handle("prefs:step_values", step.rect.center());
});
ui_kit::settings_section(ui, "holes", app.tr("prefs.holes"), |ui| {
let check = setting::metric_tap_drill_major_less_pitch(app, ui);
crate::scenario::publish_handle("prefs:metric_tap_drill_major_less_pitch", check.rect.center());
});
ui_kit::settings_section(ui, "orientation", app.tr("prefs.orientation"), |ui| {
ui.horizontal_wrapped(|ui| {
for (id, label, y_up) in [("z", "Z up", false), ("y", "Y up", true)] {
+4
View File
@@ -27,6 +27,10 @@ cutting taps, printed p.704 (M, coarse) and pp.704 to 706 (MF, fine); M8 drills
the pitch, to 0.001 mm (M8.5x1: 7.5 mm). This replaces the major-minus-pitch
estimate every metric size took before (M8 6.75 mm). A saved design keeps the
diameter it stored, so it builds as before; editing such a hole keeps its drill.
The Design preference "Metric tapped holes drill the major diameter less the
pitch" (`metric_tap_drill_major_less_pitch`, off by default) gives new metric
holes that older drill again, in the Hole dialog and for the `solid.hole` command
given no `diameter`.
Custom metric dimensions remain editable, and Custom stays selected even when its
current dimensions match a catalogue entry.