Thread work in rustfmt's layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-26 09:50:52 -04:00
parent d433ce2c9f
commit fdede5a0ae
6 changed files with 87 additions and 20 deletions
+13 -2
View File
@@ -339,7 +339,10 @@ fn thread_grooves(t: &ThreadInfo, radius: f64, face: (f64, f64), ends: [End; 2],
return Err(DocError::Invalid("thread too short".into()));
}
if len / lead > 190.0 {
return Err(DocError::Invalid(format!("a modelled thread is limited to 190 turns ({:.0} mm at this lead); use a cosmetic one", 190.0 * lead)));
return Err(DocError::Invalid(format!(
"a modelled thread is limited to 190 turns ({:.0} mm at this lead); use a cosmetic one",
190.0 * lead
)));
}
let mut g = form.groove(d, p, t.internal);
if steep == SteepFlank::Start {
@@ -453,7 +456,15 @@ fn thread_grooves(t: &ThreadInfo, radius: f64, face: (f64, f64), ends: [End; 2],
// A left-handed groove: turning the same way about the reversed axis while rising
// along it (the screw motion about −k rising −lead per turn).
let (axis, pitch) = if h > 0.0 { (k, lead) } else { (-k, -lead) };
grooves.push(GrooveShape { plane: Plane { origin: base, x, y: k }, section: section.clone(), base, axis, pitch, turns, law: law.clone() });
grooves.push(GrooveShape {
plane: Plane { origin: base, x, y: k },
section: section.clone(),
base,
axis,
pitch,
turns,
law: law.clone(),
});
}
}
Ok(grooves)
+5 -4
View File
@@ -596,10 +596,11 @@ mod tests {
for f in [ThreadForm::Metric, ThreadForm::Trapezoidal, ThreadForm::Acme] {
let (d, p) = (20.0, 2.5);
let g = f.groove(d, p, false);
let r = d / 2.0 - match f {
ThreadForm::Metric => 3.0 * p * 3f64.sqrt() / 16.0,
_ => p / 4.0,
};
let r = d / 2.0
- match f {
ThreadForm::Metric => 3.0 * p * 3f64.sqrt() / 16.0,
_ => p / 4.0,
};
assert!((g.width(p, r) - p / 2.0).abs() < 1e-9, "{f:?}");
}
}
+37 -6
View File
@@ -805,9 +805,25 @@ fn every_form_cuts_its_standard_profile() {
let t7 = 7f64.to_radians().tan() + 1.0;
let cases = [
("trapezoidal", "Tr20x4", 20.0, 4.0, 2.0 * (1.0 - 15f64.to_radians().tan()), (15.0, 15.0), 10.0 - 2.0 - tr_ac),
("acme", "3/4-6 ACME", 0.75 * inch, inch / 6.0, inch / 12.0 * (1.0 - 14.5f64.to_radians().tan()), (14.5, 14.5), 0.375 * inch - inch / 12.0 - acme_a / 2.0),
(
"acme",
"3/4-6 ACME",
0.75 * inch,
inch / 6.0,
inch / 12.0 * (1.0 - 14.5f64.to_radians().tan()),
(14.5, 14.5),
0.375 * inch - inch / 12.0 - acme_a / 2.0,
),
("buttress", "S20x4", 20.0, 4.0, (4.0 - 3.0 * t3) / 2.0, (3.0, 30.0), 10.0 - 0.86777 * 4.0),
("buttress_asme", "1.5-8 BUTT", 1.5 * inch, inch / 8.0, (inch / 8.0 - 0.6 * inch / 8.0 * t7) / 2.0, (7.0, 45.0), 0.75 * inch - 0.6 * inch / 8.0),
(
"buttress_asme",
"1.5-8 BUTT",
1.5 * inch,
inch / 8.0,
(inch / 8.0 - 0.6 * inch / 8.0 * t7) / 2.0,
(7.0, 45.0),
0.75 * inch - 0.6 * inch / 8.0,
),
];
for (form, designation, d, p, flat, (a, b), r3) in cases {
let mut volumes = Vec::new();
@@ -816,7 +832,11 @@ fn every_form_cuts_its_standard_profile() {
let len = 6.0 * p;
run(&mut s, "solid.cylinder", json!({"radius": d / 2.0, "height": len, "body_name": "Shaft"}));
let before = exact_volume(&s, "Shaft");
let th = run(&mut s, "solid.thread", json!({"face": [d / 2.0, 0, len / 2.0], "form": form, "designation": designation, "hand": hand, "modeled": true}));
let th = run(
&mut s,
"solid.thread",
json!({"face": [d / 2.0, 0, len / 2.0], "form": form, "designation": designation, "hand": hand, "modeled": true}),
);
assert!(th["thread"].is_null(), "{th}");
let st = s.model.state();
assert!(st.body("Shaft").unwrap().body.validity().is_empty(), "{designation} {hand}");
@@ -965,7 +985,11 @@ fn a_chamfered_tip_tapers_the_last_teeth() {
assert!((got - want).abs() < want * 1e-4, "{got} vs {want}");
// 30° to the axis and 0.5 mm deep, at the one free end of a shaft on a collar.
let mut s = shaft_on_collar();
run(&mut s, "solid.thread", json!({"face": [5, 0, 25], "designation": "M10", "tip": "chamfer", "chamfer_angle": "30 deg", "chamfer_depth": 0.5, "modeled": true}));
run(
&mut s,
"solid.thread",
json!({"face": [5, 0, 25], "designation": "M10", "tip": "chamfer", "chamfer_angle": "30 deg", "chamfer_depth": 0.5, "modeled": true}),
);
let w = ext_half_width(10.0, 1.5);
let rf = 4.5;
let slope = 30f64.to_radians().tan();
@@ -976,7 +1000,10 @@ fn a_chamfered_tip_tapers_the_last_teeth() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"corner": [-10, -10, 0], "length": 20, "width": 20, "height": 10, "body_name": "Nut"}));
run(&mut s, "solid.hole", json!({"position": [0, 0, 10], "diameter": 8.5}));
let e = s.execute("solid.thread", &json!({"face": [4.25, 0, 5], "designation": "M10", "tip": "chamfer", "modeled": true})).err().map(|e| e.to_string());
let e = s
.execute("solid.thread", &json!({"face": [4.25, 0, 5], "designation": "M10", "tip": "chamfer", "modeled": true}))
.err()
.map(|e| e.to_string());
assert!(e.unwrap_or_default().contains("countersink"));
}
@@ -1022,7 +1049,11 @@ fn a_runout_fades_the_groove_out_before_each_shoulder() {
let mut s = shaft_on_collar();
run(&mut s, "solid.cylinder", json!({"base": [0, 0, 35], "radius": 8, "height": 10, "operation": "join", "targets": ["Part"]}));
let before = exact_volume(&s, "Part");
run(&mut s, "solid.thread", json!({"face": [5, 0, 20], "designation": "M10", "ease_in": "standard", "ease_out": "short", "from": [0, 0, 10], "modeled": true}));
run(
&mut s,
"solid.thread",
json!({"face": [5, 0, 20], "designation": "M10", "ease_in": "standard", "ease_out": "short", "from": [0, 0, 10], "modeled": true}),
);
let st = s.model.state();
assert!(st.body("Part").unwrap().body.validity().is_empty());
let (p, w) = (1.5, ext_half_width(10.0, 1.5));
+20 -3
View File
@@ -23,12 +23,22 @@ pub fn sweep_helix(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec
/// `offset(t)` mm away from the axis (negative: towards it) in its half-plane, `offset` piecewise
/// linear through `law`'s (turn, offset) pairs, turns ascending within [0, `turns`]. A thread's
/// groove fading in or out over a runout: SMLib's radial screw sweep.
pub fn sweep_helix_radial(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec3, pitch: f64, turns: f64, law: &[(f64, f64)]) -> Result<Body> {
pub fn sweep_helix_radial(
plane: &Plane,
region: &Region2,
axis_origin: Vec3,
axis: Vec3,
pitch: f64,
turns: f64,
law: &[(f64, f64)],
) -> Result<Body> {
if law.is_empty() {
return sweep_helix(plane, region, axis_origin, axis, pitch, turns);
}
let (k, profile) = helix_profile(plane, region, axis_origin, axis, pitch, turns)?;
if !law.windows(2).all(|w| matches!(w, [a, b] if b.0 > a.0)) || law.iter().any(|(t, o)| !(t.is_finite() && o.is_finite() && *t >= 0.0 && *t <= turns)) {
if !law.windows(2).all(|w| matches!(w, [a, b] if b.0 > a.0))
|| law.iter().any(|(t, o)| !(t.is_finite() && o.is_finite() && *t >= 0.0 && *t <= turns))
{
return Err(KernelError::Invalid("helix: the radial law's turns must ascend within the sweep's turns".into()));
}
crate::smlib_sweeps::screw_radial(plane, &crate::smlib_sweeps::pieces([&profile]), axis_origin, k, pitch, turns, law)
@@ -36,7 +46,14 @@ pub fn sweep_helix_radial(plane: &Plane, region: &Region2, axis_origin: Vec3, ax
/// A helical sweep's axis and profile (the region's outer loop), checked: finite turns and
/// pitch, an area, and a profile that clears itself after one turn.
fn helix_profile(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec3, pitch: f64, turns: f64) -> Result<(Vec3, crate::numbered::NumberedLoop)> {
fn helix_profile(
plane: &Plane,
region: &Region2,
axis_origin: Vec3,
axis: Vec3,
pitch: f64,
turns: f64,
) -> Result<(Vec3, crate::numbered::NumberedLoop)> {
let k = axis.normalized().ok_or_else(|| KernelError::Invalid("helix axis".into()))?;
if !(pitch.is_finite() && turns.is_finite() && turns > 0.0 && turns <= 200.0 && pitch.abs() < 1e7 && axis_origin.is_finite()) {
return Err(KernelError::Invalid("helix: turns must be in (0, 200] and the pitch finite".into()));
+11 -3
View File
@@ -71,8 +71,8 @@ pub(crate) struct Draft {
}
pub(crate) use imp::{
body_from_record, curve, extrude, face_section, loft, loft_rails, loft_to_point, path_curve, project_onto_face, revolve, screw, screw_radial, sphere, sweep,
sweep_controlled,
body_from_record, curve, extrude, face_section, loft, loft_rails, loft_to_point, path_curve, project_onto_face, revolve, screw, screw_radial,
sphere, sweep, sweep_controlled,
};
/// Refuses what no SMLib sweep does, before any work.
@@ -1267,7 +1267,15 @@ mod imp {
/// [`screw`] with the profile moved `offset(t)` mm off the axis at turn t (negative: towards
/// it), piecewise linear through `law`'s (turn, offset) pairs: a groove fading over a runout.
pub(crate) fn screw_radial(plane: &Plane, pieces: &[Piece], origin: Vec3, axis: Vec3, pitch: f64, turns: f64, law: &[(f64, f64)]) -> Result<Body> {
pub(crate) fn screw_radial(
plane: &Plane,
pieces: &[Piece],
origin: Vec3,
axis: Vec3,
pitch: f64,
turns: f64,
law: &[(f64, f64)],
) -> Result<Body> {
let c = curves(plane, pieces)?;
let tolerance = (pitch.abs() * 1e-4).clamp(1e-6, 1e-2);
let brep =
+1 -2
View File
@@ -365,8 +365,7 @@ pub fn material_in_ring(b: &Body, c: &CylinderFace, r_in: f64, r_out: f64, along
if !(r_in >= 0.0 && r_out > r_in && a1 > a0 && r_out.is_finite() && a0.is_finite() && a1.is_finite()) {
return Err(crate::KernelError::Invalid(format!("ring r {r_in}..{r_out} over {a0}..{a1}")));
}
let plane =
Plane::from_normal(c.axis_point + c.axis * a0, *c.axis).ok_or_else(|| crate::KernelError::Invalid("ring axis".into()))?;
let plane = Plane::from_normal(c.axis_point + c.axis * a0, *c.axis).ok_or_else(|| crate::KernelError::Invalid("ring axis".into()))?;
// Circles started at the primitives' odd seam angle, off the frame's axes (as `cylinder`).
let seam = crate::build::SEAM_ANGLE;
let holes = if r_in > 0.0 { vec![Loop2::circle_from(Vec2::ZERO, r_in, seam).reversed()] } else { Vec::new() };