mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
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:
@@ -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": "페이드 길이"
|
||||
}
|
||||
}
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, ¶ms).unwrap_or_else(|e| panic!("{made}: OK on the reopened dialog: {e}"));
|
||||
|
||||
Reference in New Issue
Block a user