Thread dialog: the Ends section is Start (None, Chamfer) and End (Square, Fade)

The thread's start is its free end: it runs off plain or chamfered (ISO 4753). Its inner end
stops square in its own profile or fades over a length; an empty Fade Length is ISO 3508's
runout (2.5 P, or 1.25 P for a thread saved short). A thread saved chamfered at both ends keeps
that. "Square" is its own message (thread.end_square), apart from the geometric square. The
new labels are in en, ar, he, ja, zh, de and ko; Extent, Ends, the chamfer labels and the ISO
4753 hint come from the catalog entries already on develop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-27 07:40:29 -04:00
committed by Irakli Kourtis
parent 5fe141c0f9
commit 3865982820
4 changed files with 132 additions and 48 deletions
+65
View File
@@ -0,0 +1,65 @@
{
"en": {
"thread.chamfer_depth_needed": "required for this form",
"thread.end_square": "Square",
"thread.fade_short": "1.25 P (ISO 3508)",
"thread.fade_standard": "2.5 P (ISO 3508)",
"ui.end": "End",
"ui.fade": "Fade",
"ui.fade_length": "Fade Length"
},
"ar": {
"thread.chamfer_depth_needed": "مطلوب لهذا الشكل",
"thread.end_square": "قائمة",
"thread.fade_short": "1.25 P (ISO 3508)",
"thread.fade_standard": "2.5 P (ISO 3508)",
"ui.end": "النهاية",
"ui.fade": "تلاشٍ",
"ui.fade_length": "طول التلاشي"
},
"he": {
"thread.chamfer_depth_needed": "נדרש לצורה זו",
"thread.end_square": "ישר",
"thread.fade_short": "1.25 P (ISO 3508)",
"thread.fade_standard": "2.5 P (ISO 3508)",
"ui.end": "סוף",
"ui.fade": "דעיכה",
"ui.fade_length": "אורך דעיכה"
},
"ja": {
"thread.chamfer_depth_needed": "この形状では必須",
"thread.end_square": "直角",
"thread.fade_short": "1.25 P(ISO 3508)",
"thread.fade_standard": "2.5 P(ISO 3508)",
"ui.end": "終端",
"ui.fade": "切り上げ",
"ui.fade_length": "切り上げ長さ"
},
"zh": {
"thread.chamfer_depth_needed": "此牙型必填",
"thread.end_square": "直角",
"thread.fade_short": "1.25 P(ISO 3508)",
"thread.fade_standard": "2.5 P(ISO 3508)",
"ui.end": "末端",
"ui.fade": "收尾",
"ui.fade_length": "收尾长度"
},
"de": {
"thread.chamfer_depth_needed": "für diese Form erforderlich",
"thread.end_square": "Rechtwinklig",
"thread.fade_short": "1,25 P (ISO 3508)",
"thread.fade_standard": "2,5 P (ISO 3508)",
"ui.end": "Ende",
"ui.fade": "Auslauf",
"ui.fade_length": "Auslauflänge"
},
"ko": {
"thread.chamfer_depth_needed": "이 형상에 필요",
"thread.end_square": "직각",
"thread.fade_short": "1.25 P (ISO 3508)",
"thread.fade_standard": "2.5 P (ISO 3508)",
"ui.end": "끝",
"ui.fade": "페이드",
"ui.fade_length": "페이드 길이"
}
}
+2 -2
View File
@@ -234,7 +234,7 @@ fn control_role(command: &str, label: &str) -> Option<egui::accesskit::Role> {
| "Bend Line Position" | "From Snap" | "To Snap" | "Joint" | "Joint 1" | "Joint 2" | "Gear 1" | "Gear 2" | "Pinion" | "Rack" | "Screw"
| "Nut" | "Cam" | "Follower" | "Anchor" => Role::ComboBox,
"Radius" if command == "joint.rack_pinion" => Role::Label,
"Pitch" | "Form" | "Hand" | "Starts" | "Extent" | "Ease Out" | "Tip" if command == "solid.thread" => Role::ComboBox,
"Pitch" | "Form" | "Hand" | "Starts" | "Extent" | "Start" | "End" if command == "solid.thread" => Role::ComboBox,
"Automatic size" | "Reverse" | "Other side" | "All Lines" | "Mirror" => Role::CheckBox,
"Animate" => Role::Button,
"Ratio" if command.starts_with("joint.") && command != "joint.motion_link" => return None,
@@ -362,7 +362,7 @@ form_case!(path_pattern, "solid.pattern.path", ["Type", "Object Type", "Quantity
form_case!(pipe, "solid.pipe", ["Diameter", "Wall", "Operation"]);
form_case!(stock, "solid.bounding_solid", ["Margin"]);
form_case!(frame_member, "frame.member", ["All Lines", "Anchor", "Angle", "Mirror", "Name"]);
form_case!(thread, "solid.thread", ["Form", "Size", "Pitch", "Designation", "Hand", "Starts", "Extent", "Ease Out", "Tip"]);
form_case!(thread, "solid.thread", ["Form", "Size", "Pitch", "Designation", "Hand", "Starts", "Extent", "Start", "End"]);
form_case!(primitive_box, "solid.box", ["length", "width", "height", "Operation"]);
form_case!(cylinder, "solid.cylinder", ["diameter", "height", "Operation"]);
form_case!(sphere, "solid.sphere", ["diameter", "Operation"]);
+45 -32
View File
@@ -173,11 +173,11 @@ const FULL: usize = 0;
const LENGTH: usize = 1;
const UP_TO_EXTENT: usize = 2;
const EXTENTS: &[(&str, &str)] = &[("Full", "full"), ("Length", "length"), ("Up to", "up_to")];
/// 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;
/// Which free ends are chamfered (ISO 4753).
const TIPS: &[(&str, ThreadTip)] = &[("None", ThreadTip::None), ("Start", ThreadTip::Start), ("Both", ThreadTip::Both)];
/// The thread's start, its free end: run off it plain, or chamfered (ISO 4753).
const STARTS_AT: &[&str] = &["None", "Chamfer"];
/// The thread's inner end: a square stop (the groove ends in its own profile) or a fade
/// (ISO 3508's runout).
const ENDS_AT: &[&str] = &["thread.end_square", "ui.fade"];
const FLANKS: &[(&str, SteepFlank)] = &[("End", SteepFlank::End), ("Start", SteepFlank::Start)];
/// The pick of the end the thread is measured from (`from`).
@@ -191,22 +191,34 @@ pub(crate) fn inputs() -> Vec<SelInput> {
vec![SelInput::new("Cylindrical face", FACES, false)]
}
/// A runout at one end: a choice from the runout list and the typed length.
/// The inner end: square or a fade, the fade's typed length (empty: ISO 3508's runout, the
/// standard 2.5 P unless the thread was saved with the short 1.25 P).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Ease {
pub choice: usize,
pub fade: bool,
pub length: String,
pub short: bool,
}
impl Ease {
fn of(r: &Option<ThreadRunout>) -> Ease {
match r {
None => Ease::default(),
Some(ThreadRunout::Standard) => Ease { choice: 1, length: String::new() },
Some(ThreadRunout::Short) => Ease { choice: 2, length: String::new() },
Some(ThreadRunout::Length { length }) => Ease { choice: EASE_LENGTH, length: expr(length) },
Some(ThreadRunout::Standard) => Ease { fade: true, ..Ease::default() },
Some(ThreadRunout::Short) => Ease { fade: true, short: true, ..Ease::default() },
Some(ThreadRunout::Length { length }) => Ease { fade: true, length: expr(length), short: false },
}
}
/// The command's `ease_out`, none for a square end.
fn param(&self) -> Option<Value> {
let typed = self.length.trim();
self.fade.then(|| match (typed.is_empty(), self.short) {
(false, _) => json!(typed),
(true, true) => json!("short"),
(true, false) => json!("standard"),
})
}
}
fn expr(e: &Expr) -> String {
@@ -248,7 +260,7 @@ impl Options {
/// Does the thread start at one end of the face (a length or an Up To extent, a runout or a
/// tip)? Then the end to start from can be picked.
pub fn starts_at_an_end(&self) -> bool {
self.extent != FULL || self.ease_out.choice != 0 || self.tip != ThreadTip::None
self.extent != FULL || self.ease_out.fade || self.tip != ThreadTip::None
}
}
@@ -400,30 +412,33 @@ pub(crate) fn rows(
}
crate::ui_kit::section(ui, "thread_ends", "Ends", true, |ui| {
enter |= ease_rows(ui, "Ease Out", "thread_ease_out", &mut o.ease_out);
pick(ui, "Tip", "thread_tip", &mut o.tip, TIPS);
// The start, a free end: plain or chamfered. A thread saved chamfered at both ends
// keeps that.
let mut chamfered = usize::from(o.tip != ThreadTip::None);
choice_row(ui, "Start", "thread_start", &mut chamfered, STARTS_AT);
o.tip = match (chamfered, o.tip) {
(0, _) => ThreadTip::None,
(_, ThreadTip::None) => ThreadTip::Start,
(_, kept) => kept,
};
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"));
// ISO 4753's depth is for ISO metric threads; the other forms give theirs.
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, reset)
}
/// A runout choice, with its length when typed.
fn ease_rows(ui: &mut egui::Ui, label: &'static str, id: &str, e: &mut Ease) -> bool {
let labels: Vec<&str> = EASES.iter().map(|x| x.0).collect();
let before = e.choice;
choice_row(ui, label, id, &mut e.choice, &labels);
if e.choice != EASE_LENGTH {
return false;
}
if before != EASE_LENGTH && e.length.trim().is_empty() {
e.length = "1 mm".into();
}
expr_row(ui, "Ease Out Length", &mut e.length)
}
/// 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>) {
@@ -522,10 +537,8 @@ pub(crate) fn put_params(p: &mut Value, length: &str, o: &Options, inputs: &[Sel
if let Some(from) = object_point(inputs, START_END) {
p["from"] = from;
}
match o.ease_out.choice {
EASE_LENGTH => put(p, &[("ease_out", o.ease_out.length.as_str())]),
0 => {}
c => p["ease_out"] = json!(EASES[c].1),
if let Some(e) = o.ease_out.param() {
p["ease_out"] = e;
}
if o.tip != ThreadTip::None {
p["tip"] = json!(o.tip);
+20 -14
View File
@@ -73,9 +73,9 @@ fn every_option_reaches_the_command() {
choose(&mut h, "Starts", "2");
choose(&mut h, "Extent", "Length");
type_in(&mut h, "Length", "6 mm");
choose(&mut h, "Ease Out", "Length");
type_in(&mut h, "Ease Out Length", "1.5 mm");
choose(&mut h, "Tip", "Both");
choose(&mut h, "End", "ui.fade");
type_in(&mut h, "Fade Length", "1.5 mm");
choose(&mut h, "Start", "Chamfer");
type_in(&mut h, "Chamfer Angle", "30 deg");
type_in(&mut h, "Chamfer Depth", "0.5 mm");
click(&mut h, &["dialog:Modeled"]);
@@ -85,14 +85,20 @@ fn every_option_reaches_the_command() {
p,
json!({
"designation": "Tr20x4", "form": "trapezoidal", "hand": "left", "starts": 2, "extent": "length", "length": "6 mm",
"ease_out": "1.5 mm", "tip": "both", "chamfer_angle": "30 deg", "chamfer_depth": "0.5 mm",
"ease_out": "1.5 mm", "tip": "start", "chamfer_angle": "30 deg", "chamfer_depth": "0.5 mm",
"modeled": true,
})
);
choose(&mut h, "Ease Out", "Short (1.25 P)");
// An empty fade length is ISO 3508's standard runout; a square end has none.
click(&mut h, &["dialog:Fade Length"]);
h.step(&json!({"key":"A","cmd":true})).unwrap();
h.step(&json!({"key":"Backspace"})).unwrap();
h.frames(2);
choose(&mut h, "Hand", "Both");
let p = params(&h);
assert_eq!((&p["ease_out"], &p["hand"]), (&json!("short"), &json!("both")));
assert_eq!((&p["ease_out"], &p["hand"]), (&json!("standard"), &json!("both")));
choose(&mut h, "End", "thread.end_square");
assert!(params(&h).get("ease_out").is_none());
}
/// Buttress rows only for the buttress forms, the length only with a Length extent, the Up To
@@ -101,10 +107,10 @@ fn every_option_reaches_the_command() {
#[test]
fn only_the_fields_that_apply_are_shown() {
let mut h = shaft();
for row in ["Form", "Size", "Pitch", "Designation", "Hand", "Starts", "Extent", "Ease Out", "Tip"] {
for row in ["Form", "Size", "Pitch", "Designation", "Hand", "Starts", "Extent", "Start", "End"] {
assert!(shown(row), "{row}");
}
for row in ["Steep Flank", "Length", "Ease Out Length", "Chamfer Angle", "Chamfer Depth"] {
for row in ["Steep Flank", "Length", "Fade Length", "Chamfer Angle", "Chamfer Depth"] {
assert!(!shown(row), "{row}");
}
let inputs = |h: &Harness| h.app.dialog.as_ref().unwrap().inputs.iter().map(|i| i.label).collect::<Vec<_>>();
@@ -129,10 +135,10 @@ fn only_the_fields_that_apply_are_shown() {
assert!(shown("Length"));
choose(&mut h, "Extent", "Full");
assert_eq!(inputs(&h), ["Cylindrical face"]);
choose(&mut h, "Ease Out", "Length");
choose(&mut h, "End", "ui.fade");
assert_eq!(inputs(&h), ["Cylindrical face", START_END], "a runout starts at an end");
assert!(shown("Ease Out Length"));
choose(&mut h, "Tip", "Start");
assert!(shown("Fade Length"));
choose(&mut h, "Start", "Chamfer");
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}");
@@ -177,7 +183,7 @@ fn a_form_change_keeps_what_still_applies() {
choose(&mut h, "Starts", "3");
choose(&mut h, "Extent", "Length");
type_in(&mut h, "Length", "4 mm");
choose(&mut h, "Ease Out", "Short (1.25 P)");
choose(&mut h, "End", "ui.fade");
let kept = options(&h);
choose(&mut h, "Form", "Trapezoidal (ISO 2904)");
@@ -195,7 +201,7 @@ fn a_form_change_keeps_what_still_applies() {
let now = options(&h);
assert_eq!(Options { form: kept.form, ..now.clone() }, kept);
let p = params(&h);
assert_eq!((&p["hand"], &p["starts"], &p["length"], &p["ease_out"]), (&json!("left"), &json!(3), &json!("4 mm"), &json!("short")));
assert_eq!((&p["hand"], &p["starts"], &p["length"], &p["ease_out"]), (&json!("left"), &json!(3), &json!("4 mm"), &json!("standard")));
}
/// The dialog shows the command's own error text for options it cannot build.
@@ -276,7 +282,7 @@ fn a_saved_thread_reopens_with_every_option() {
assert_eq!(p["extent"], extent);
if made["designation"] == "S20x4" {
assert_eq!(p["form"], "buttress", "the form is saved resolved");
assert!(shown("Steep Flank") && shown("Chamfer Angle") && shown("Ease Out Length"));
assert!(shown("Steep Flank") && shown("Chamfer Angle") && shown("Fade Length"));
}
let (cmd, params) = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap().remove(0);
h.app.session.execute(&cmd, &params).unwrap_or_else(|e| panic!("{made}: OK on the reopened dialog: {e}"));