mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
thread: a shaft thicker than a modelled thread is refused, never turned down
A modelled thread never changes the size of the shaft it is on (#1018, #2853): a shaft thicker than the thread is refused with the size to make it, as tests/kernel-cases/threads/2705 expects ("this 14.6 mm shaft is too thick for a modelled M14 thread; make the shaft 14 mm for M14"). This takes out the turn-down of #4256: `thread_turn`, the fit that let a thicker shaft through, and its tests; the 9 mm shaft refusing an M8 and the preview's M6 refusal on an 8 mm shaft are back as they were before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
This commit is contained in:
@@ -206,8 +206,7 @@ pub(super) fn thread_on(st: &ModelState, feature: u64, p: Vec3, ask: &ThreadAsk)
|
||||
} else if cyl.internal {
|
||||
(minor - dia).abs() < 0.2 * major || (form.drill(major, pitch) - dia).abs() < 0.15 * major
|
||||
} else {
|
||||
// A modelled thread also fits a shaft thicker than it, which it turns down first.
|
||||
(major - dia).abs() < 0.15 * major || (ask.modeled && dia > major)
|
||||
(major - dia).abs() < 0.15 * major
|
||||
};
|
||||
if !fits {
|
||||
return Err(DocError::Invalid(format!("an {designation} thread does not fit a {dia:.2} mm {}", if cyl.internal { "hole" } else { "shaft" })));
|
||||
@@ -493,73 +492,6 @@ fn thread_grooves(t: &ThreadInfo, radius: f64, face: (f64, f64), ends: [End; 2],
|
||||
|
||||
/// Evaluate a thread feature: a cosmetic thread is listed in the model's threads, a modelled
|
||||
/// one cut into its body.
|
||||
/// What a modelled external thread turns off a shaft thicker than its major diameter before it
|
||||
/// cuts, as a lathe would: the ring between the major diameter and the shaft over the threaded
|
||||
/// length, run on past an end of the face that nothing of the part (`mb`) lies beyond. `None`
|
||||
/// for a shaft no thicker than the thread. Its inner wall is the turned face, named `turned`;
|
||||
/// an end that stops inside the face leaves a shoulder, `shoulder:start` or `shoulder:end`.
|
||||
fn thread_turn(t: &ThreadInfo, cyl: &kernel::CylinderFace, mb: &ModelBody) -> Result<Option<Traced>> {
|
||||
let (p, r) = (t.pitch, t.major_diameter / 2.0);
|
||||
if t.internal || !(cyl.radius > r * (1.0 + 1e-6)) {
|
||||
return Ok(None);
|
||||
}
|
||||
let k = t.axis;
|
||||
let u = k.cross(if k.x.abs() < 0.9 { Vec3::new(1.0, 0.0, 0.0) } else { Vec3::new(0.0, 1.0, 0.0) });
|
||||
let u = u.normalized().ok_or_else(|| DocError::Invalid("thread axis".into()))?;
|
||||
let v = k.cross(u);
|
||||
let mesh = mb.mesh();
|
||||
let solid_round = |s: f64, radius: f64| {
|
||||
(0..12).any(|i| {
|
||||
let a = std::f64::consts::TAU * f64::from(i) / 12.0;
|
||||
mesh.contains(t.axis_point + *k * s + (u * a.cos() + v * a.sin()) * radius)
|
||||
})
|
||||
};
|
||||
// The ring reaches a little past the shaft; the part must not go on there (a shaft standing
|
||||
// in a corner, a rounded edge), or the turning would cut into it.
|
||||
let out = cyl.radius + 0.1 * p;
|
||||
if [0.25, 0.5, 0.75].iter().any(|f| solid_round(t.start + (t.end - t.start) * f, cyl.radius + 0.05 * p)) {
|
||||
return Err(DocError::Invalid(format!(
|
||||
"this {} mm shaft is too thick for a modelled {} thread, and the part around it keeps it from being turned down; make the shaft {} mm",
|
||||
show_mm(2.0 * cyl.radius),
|
||||
t.designation,
|
||||
show_mm(t.major_diameter)
|
||||
)));
|
||||
}
|
||||
// Past a free end of the face out into the air; elsewhere square at the threaded length's end.
|
||||
let tol = p * 1e-3;
|
||||
let mid = (r + cyl.radius) / 2.0;
|
||||
let free = |at: f64, s: f64, past: f64| (at - s).abs() < tol && !solid_round(s + past * 0.5 * p, mid);
|
||||
let (open0, open1) = (free(cyl.start, t.start, -1.0), free(cyl.end, t.end, 1.0));
|
||||
let s0 = if open0 { t.start - p } else { t.start };
|
||||
let s1 = if open1 { t.end + p } else { t.end };
|
||||
let plane = Plane { origin: t.axis_point, x: u, y: *k };
|
||||
let ring = [Vec2::new(r, s0), Vec2::new(out, s0), Vec2::new(out, s1), Vec2::new(r, s1)];
|
||||
let region = Region2 { outer: solidcraft_geom::Loop2::polygon(&ring), holes: vec![] };
|
||||
let ring = kernel::revolve(&plane, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| DocError::Invalid("thread: the shaft turned down to the thread".into()))?;
|
||||
// The ring's sides by profile segment: its start end, outside, end end, inner wall.
|
||||
let role = |segment: usize| -> Vec<String> {
|
||||
match segment {
|
||||
0 if !open0 => vec!["shoulder:start".into()],
|
||||
2 if !open1 => vec!["shoulder:end".into()],
|
||||
3 => vec!["turned".into()],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
};
|
||||
let roles = ring.history().filter(|h| h.operands == 0).map(|h| {
|
||||
h.faces
|
||||
.iter()
|
||||
.map(|f| match f.origins.as_slice() {
|
||||
[kernel::history::FaceOrigin::Profile { segment }] => role(*segment),
|
||||
_ => Vec::new(),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
Ok(Some(Traced::made(ring, roles)))
|
||||
}
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
let FeatureKind::Thread {
|
||||
face,
|
||||
@@ -624,16 +556,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
}
|
||||
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())) };
|
||||
// A shaft thicker than the thread is turned down to its major diameter first.
|
||||
let input = mb.traced();
|
||||
let (input, radius) = match thread_turn(&t, &cyl, &mb)? {
|
||||
Some(ring) => {
|
||||
let turned = kernel::boolean(&input.body, &ring.body, BoolOp::Cut)?
|
||||
.ok_or_else(|| DocError::Invalid("turning the shaft down removed everything".into()))?;
|
||||
(Traced::step(turned, &[&input, &ring]), t.major_diameter / 2.0)
|
||||
}
|
||||
None => (input, cyl.radius),
|
||||
};
|
||||
let (input, radius) = (mb.traced(), cyl.radius);
|
||||
// The exact grooves: SMLib's screw sweeps, cut by one B-rep boolean.
|
||||
let mut grooves = Vec::new();
|
||||
for g in thread_grooves(&t, radius, (cyl.start, cyl.end), ends, steep, ease)? {
|
||||
|
||||
@@ -93,7 +93,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.thread", "Thread", thread)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("thread")
|
||||
.params("face: [x,y,z] on a cylindrical face; form?: metric|trapezoidal|acme|buttress|buttress_asme (default: the one `designation` names, else metric; square is refused: no current standard defines it); designation?: the size, as its standard writes it: metric \"M8\", \"M8x1\", \"M16xPh3P1.5\" (ISO 68-1, ISO 261, ISO 965-1); trapezoidal \"Tr20x4\", \"Tr40x14(P7)\" (ISO 2904, ISO 2903); buttress \"S20x4\", \"S40x14(P7)\" (DIN 513, 3°/30°); acme \"1/2-10 ACME\" (ASME B1.5); buttress_asme \"2.5-8 BUTT\" (ASME B1.9, 7°/45°, basic profile; no size table) (default: the standard size that fits the face, a shaft its major diameter, a hole its drill); hand?: right|left|both (default right; both: a right- and a left-handed groove crossing); steep_flank?: start|end (buttress only, default end: the steep flank of the screw's teeth faces the thread's end, the way its axis points); starts?: 1..8 (lead = starts x pitch: that many grooves, each a turn's starts-th further round; a designation that gives the lead gives the starts); ease_in?, ease_out?: expr|standard|short (modelled threads: the groove fades out over this length, its section moving out of the material as it turns; standard 2.5 P, short 1.25 P per ISO 3508; ease_in at the end the thread starts from (`from`, or the start of its extent), ease_out at the other; it lies past the full-form length, up to a shoulder's clearance; at a free end, inside the face); tip?: none|chamfer (modelled external threads: each free end the thread reaches is chamfered after the groove is cut, ISO 4753's chamfered end, the last teeth tapering into the cone; chamfer_angle?: angle expr to the axis, default 45 deg; chamfer_depth?: expr, radial from the major diameter, default down to the external minor diameter, the largest ISO 4753 allows); extent?: full|length|up_to (default: length when `length` is given, up_to when `up_to` is, else full: the whole face); length?: expr (from the end the thread starts at); up_to?: [x,y,z] (a point, such as on a shoulder face: the thread runs to its place along the axis) | plane name or {origin, normal} (to where the plane crosses the axis); from?: [x,y,z] near the end of the face the thread starts at (default: the face's only free end, the one with no material past it; else the end the `face` point lies within a third of the face of; a pick in the middle of a face whose ends are alike is refused); modeled?: bool (default false). Cosmetic: the model is unchanged, model.threads lists it. Modeled: cuts the thread profile into the body: past a free end the groove runs out a pitch into the air (or through a chamfer); at a shoulder, a head or a blind hole's bottom it stops with its section clear of it, never cutting into it; a shaft thicker than the thread is first turned down to its major diameter over the threaded length"),
|
||||
.params("face: [x,y,z] on a cylindrical face; form?: metric|trapezoidal|acme|buttress|buttress_asme (default: the one `designation` names, else metric; square is refused: no current standard defines it); designation?: the size, as its standard writes it: metric \"M8\", \"M8x1\", \"M16xPh3P1.5\" (ISO 68-1, ISO 261, ISO 965-1); trapezoidal \"Tr20x4\", \"Tr40x14(P7)\" (ISO 2904, ISO 2903); buttress \"S20x4\", \"S40x14(P7)\" (DIN 513, 3°/30°); acme \"1/2-10 ACME\" (ASME B1.5); buttress_asme \"2.5-8 BUTT\" (ASME B1.9, 7°/45°, basic profile; no size table) (default: the standard size that fits the face, a shaft its major diameter, a hole its drill); hand?: right|left|both (default right; both: a right- and a left-handed groove crossing); steep_flank?: start|end (buttress only, default end: the steep flank of the screw's teeth faces the thread's end, the way its axis points); starts?: 1..8 (lead = starts x pitch: that many grooves, each a turn's starts-th further round; a designation that gives the lead gives the starts); ease_in?, ease_out?: expr|standard|short (modelled threads: the groove fades out over this length, its section moving out of the material as it turns; standard 2.5 P, short 1.25 P per ISO 3508; ease_in at the end the thread starts from (`from`, or the start of its extent), ease_out at the other; it lies past the full-form length, up to a shoulder's clearance; at a free end, inside the face); tip?: none|chamfer (modelled external threads: each free end the thread reaches is chamfered after the groove is cut, ISO 4753's chamfered end, the last teeth tapering into the cone; chamfer_angle?: angle expr to the axis, default 45 deg; chamfer_depth?: expr, radial from the major diameter, default down to the external minor diameter, the largest ISO 4753 allows); extent?: full|length|up_to (default: length when `length` is given, up_to when `up_to` is, else full: the whole face); length?: expr (from the end the thread starts at); up_to?: [x,y,z] (a point, such as on a shoulder face: the thread runs to its place along the axis) | plane name or {origin, normal} (to where the plane crosses the axis); from?: [x,y,z] near the end of the face the thread starts at (default: the face's only free end, the one with no material past it; else the end the `face` point lies within a third of the face of; a pick in the middle of a face whose ends are alike is refused); modeled?: bool (default false). Cosmetic: the model is unchanged, model.threads lists it. Modeled: cuts the thread profile into the body: past a free end the groove runs out a pitch into the air (or through a chamfer); at a shoulder, a head or a blind hole's bottom it stops with its section clear of it, never cutting into it; a shaft thicker than the thread is refused with the size to make it, never turned down"),
|
||||
CommandSpec::new("solid.shell", "Shell", shell)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("shell")
|
||||
|
||||
@@ -362,66 +362,25 @@ fn modelled_full_length_fine_threads_on_large_shafts() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A modelled thread on a shaft thicker than it turns the shaft down to the major diameter
|
||||
/// over the threaded length, then cuts the thread: an M6 over the top 8 mm of an 8 mm shaft
|
||||
/// leaves a 6 mm threaded section on an 8 mm stub, its turned face named for later features.
|
||||
/// A shaft of the thread's own size is cut as it is.
|
||||
/// Issue 2705: an M14 over the top 10 mm of a 14.6 mm boss on a plate. A modelled thread never
|
||||
/// changes the size of the shaft it is on (#1018, #2853): it is refused with the size to make
|
||||
/// the shaft, and the part is unchanged (tests/kernel-cases/threads/2705).
|
||||
#[test]
|
||||
fn a_shaft_thicker_than_a_modelled_thread_is_turned_down_to_it() {
|
||||
let (p, len) = (1.0, 8.0);
|
||||
let root = 3.0 - 5.0 * p * 3f64.sqrt() / 16.0;
|
||||
let groove = per_turn(root, 3.0, |r| p / 4.0 + 2.0 * (r - root) * (PI / 6.0).tan()) * len / p;
|
||||
for (shaft, turned) in [(8.0, true), (6.0, false)] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": shaft / 2.0, "height": 12, "body_name": "Shaft"}));
|
||||
run(&mut s, "solid.thread", json!({"face": [shaft / 2.0, 0, 11], "designation": "M6", "length": len, "modeled": true}));
|
||||
let ring = PI * (shaft * shaft / 4.0 - 9.0) * len;
|
||||
let want = PI * shaft * shaft / 4.0 * 12.0 - ring - groove;
|
||||
let got = volume(&mut s, "Shaft");
|
||||
assert!((got - want).abs() < groove * 0.03, "M6 on {shaft} mm: {got} mm³, want {want}");
|
||||
let st = s.model.state();
|
||||
let Some(b) = st.body("Shaft") else { panic!("no shaft") };
|
||||
assert!(b.body.validity().is_empty(), "{shaft} mm: {:?}", b.body.validity());
|
||||
// Nothing past the major diameter over the threaded length; the stub keeps its size.
|
||||
let round = |z: f64, r: f64| {
|
||||
(0..24).filter(|i| b.body.contains(Vec3::new(r * (PI * *i as f64 / 12.0).cos(), r * (PI * *i as f64 / 12.0).sin(), z))).count()
|
||||
};
|
||||
assert_eq!(round(8.0, 3.05), 0, "{shaft} mm: the threaded part is turned to 6 mm");
|
||||
assert_eq!(round(2.0, shaft / 2.0 - 0.05), 24, "{shaft} mm: the stub is whole");
|
||||
let names = solidcraft_doc::naming::face_names(b);
|
||||
let fid = s.doc.features.last().map(|f| f.id).unwrap_or(0);
|
||||
let named = |role: &str| names.iter().any(|n| n.starts_with(&format!("F{fid}:{role}")));
|
||||
assert_eq!(named("turned"), turned, "{shaft} mm: {names:?}");
|
||||
assert_eq!(named("shoulder:"), turned, "{shaft} mm: {names:?}");
|
||||
}
|
||||
|
||||
// A 9 mm shaft is close enough to M8 to be offered it; it was refused as too thick (#2688).
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 4.5, "height": 10, "body_name": "Shaft"}));
|
||||
run(&mut s, "solid.thread", json!({"face": [4.5, 0, 5], "designation": "M8", "modeled": true}));
|
||||
let root = 4.0 - 5.0 * 1.25 * 3f64.sqrt() / 16.0;
|
||||
let groove = per_turn(root, 4.0, |r| 1.25 / 4.0 + 2.0 * (r - root) * (PI / 6.0).tan()) * 10.0 / 1.25;
|
||||
let (want, got) = (PI * 16.0 * 10.0 - groove, volume(&mut s, "Shaft"));
|
||||
assert!((got - want).abs() < groove * 0.03, "M8 on 9 mm: {got} mm³, want {want}");
|
||||
}
|
||||
|
||||
/// Issue 2705: an M14 over the top 10 mm of a 14.6 mm boss on a plate. Turned to 14 mm first:
|
||||
/// 1600 + π 7.3² 15 − π (7.3² − 7²) 10 − 5 turns of 50.189 mm³ (tests/kernel-cases/threads/2705).
|
||||
#[test]
|
||||
fn an_m14_thread_on_a_14_6_mm_boss_turns_it_to_14_mm() {
|
||||
fn an_m14_thread_on_a_14_6_mm_boss_is_refused_with_the_size_to_make_it() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"corner": [-10, -10, 0], "length": 20, "width": 20, "height": 4, "body_name": "Part"}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 7.3, "height": 15, "base": [0, 0, 4], "operation": "join", "targets": ["Part"]}));
|
||||
run(&mut s, "solid.thread", json!({"face": [7.3, 0, 17], "designation": "M14", "length": 10, "modeled": true}));
|
||||
let got = volume(&mut s, "Part");
|
||||
assert!((got - 3725.5127).abs() < 3725.5127 * 0.01, "{got} mm³");
|
||||
let before = volume(&mut s, "Part");
|
||||
let thread = json!({"face": [7.3, 0, 17], "designation": "M14", "length": 10, "modeled": true});
|
||||
let e = s.execute("solid.thread", &thread).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("this 14.6 mm shaft is too thick for a modelled M14 thread; make the shaft 14 mm for M14"), "{e}");
|
||||
assert!((volume(&mut s, "Part") - before).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A shaft thinner than the thread's minor diameter is still refused, and so is a cosmetic
|
||||
/// thread on a shaft thicker than it (a note changes nothing to turn). A shaft the part goes on
|
||||
/// round (standing against a wall) is not turned into the wall.
|
||||
/// A shaft thinner than the thread's minor diameter is refused, and so is a thread on a shaft
|
||||
/// far thicker than it.
|
||||
#[test]
|
||||
fn a_thin_shaft_or_one_the_part_surrounds_is_refused() {
|
||||
fn a_shaft_far_off_the_threads_size_is_refused() {
|
||||
let refused = |s: &mut Session, body: &str, p: Value| {
|
||||
let before = volume(s, body);
|
||||
let e = s.execute("solid.thread", &p).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
@@ -438,11 +397,8 @@ fn a_thin_shaft_or_one_the_part_surrounds_is_refused() {
|
||||
let e = refused(&mut s, "Shaft", json!({"face": [4, 0, 5], "designation": "M6"}));
|
||||
assert!(e.contains("an M6 thread does not fit a 8.00 mm shaft"), "{e}");
|
||||
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"corner": [2, -10, 0], "length": 10, "width": 20, "height": 10, "body_name": "Part"}));
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 4, "height": 10, "operation": "join", "targets": ["Part"]}));
|
||||
let e = refused(&mut s, "Part", json!({"face": [-4, 0, 5], "designation": "M6", "modeled": true}));
|
||||
assert!(e.contains("this 8 mm shaft is too thick for a modelled M6 thread") && e.contains("part around it"), "{e}");
|
||||
let e = refused(&mut s, "Shaft", json!({"face": [4, 0, 5], "designation": "M6", "modeled": true}));
|
||||
assert!(e.contains("an M6 thread does not fit a 8.00 mm shaft"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -566,9 +522,9 @@ fn a_modelled_thread_that_would_cut_nothing_is_refused() {
|
||||
|
||||
/// Issue 2688: an M8 modelled thread in a 6 mm hole (narrower than its 6.65 mm minor diameter)
|
||||
/// was refused as "the face is too far from the thread's diameter". It says what is wrong and
|
||||
/// what to drill, as a hole that is too wide does. (A shaft too thick is turned down.)
|
||||
/// what to drill, as a hole that is too wide does; and the same for a shaft that is too thick.
|
||||
#[test]
|
||||
fn a_hole_too_narrow_says_so_and_what_fits() {
|
||||
fn a_hole_too_narrow_or_a_shaft_too_thick_says_which_and_what_fits() {
|
||||
let mut s = hex_nut(6.0);
|
||||
let before = volume(&mut s, "Nut");
|
||||
let e = s.execute("solid.thread", &json!({"face": [3.0, 0, 2], "designation": "M8", "modeled": true})).err().map(|e| e.to_string());
|
||||
@@ -576,6 +532,13 @@ fn a_hole_too_narrow_says_so_and_what_fits() {
|
||||
assert!(e.contains("this 6 mm hole is too narrow for a modelled M8 thread") && e.contains("drill 6.75 mm for M8"), "{e}");
|
||||
assert!(!e.contains("too far"), "{e}");
|
||||
assert!((volume(&mut s, "Nut") - before).abs() < 1e-9);
|
||||
|
||||
let mut s = Session::default();
|
||||
// 9 mm is close enough to M8 to be offered it, too thick to cut it.
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 4.5, "height": 10, "body_name": "Shaft"}));
|
||||
let e = s.execute("solid.thread", &json!({"face": [4.5, 0, 5], "designation": "M8", "modeled": true})).err().map(|e| e.to_string());
|
||||
let e = e.unwrap_or_default();
|
||||
assert!(e.contains("this 9 mm shaft is too thick for a modelled M8 thread") && e.contains("make the shaft 8 mm for M8"), "{e}");
|
||||
}
|
||||
|
||||
/// The limit is a quarter of the basic depth 5H/8: a hole up to 4.78 mm for M5 (4.86 mm for
|
||||
|
||||
@@ -1585,19 +1585,7 @@ fn modelled_thread_preview_updates_reports_errors_and_cancels_without_changes()
|
||||
designation(&mut h, "M8x1.3");
|
||||
wait_feature_preview(&mut h);
|
||||
assert_ne!(h.app.preview.key, first_key, "changing pitch updates the preview before OK");
|
||||
// An 8 mm shaft is turned down for an M6.
|
||||
let m8_key = h.app.preview.key;
|
||||
designation(&mut h, "M6x1.3");
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
|
||||
while h.app.preview.key == m8_key && std::time::Instant::now() < deadline {
|
||||
h.frames(2);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
wait_feature_preview(&mut h);
|
||||
assert!(h.app.preview.error.is_none(), "{:?}", h.app.preview.error);
|
||||
assert_eq!(h.app.preview.replaced, ["Body1"]);
|
||||
// A shaft thinner than the thread is still refused.
|
||||
designation(&mut h, "M10x1.5");
|
||||
for _ in 0..500 {
|
||||
h.frames(2);
|
||||
if !h.app.preview.busy && h.app.preview.error.is_some() {
|
||||
@@ -1607,7 +1595,7 @@ fn modelled_thread_preview_updates_reports_errors_and_cancels_without_changes()
|
||||
}
|
||||
assert!(h.app.preview.error.as_deref().is_some_and(|e| e.contains("does not fit")), "{:?}", h.app.preview.error);
|
||||
designation(&mut h, "M8x1.3");
|
||||
// The M10 error stays until the new preview starts; wait for it to go, or
|
||||
// The M6 error stays until the new preview starts; wait for it to go, or
|
||||
// `wait_feature_preview` would stop at the stale error on a host that schedules the
|
||||
// preview a few frames later.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
|
||||
|
||||
Reference in New Issue
Block a user