mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 03:01:58 +00:00
* claim: #519 thread flank continuity * Threads: keep each flank as one curved face * Threads: preserve saved flank faces when rebuilding old designs Keep historical thread faces and construction order when a saved feature has no continuous_flanks field. New threads use continuous flanks by default, with a preference for the historical layout. Redefine and dialog reopen retain the feature's saved choice. Generic coils and hole features keep their existing construction. Captured pre-change flank names and points remain on the original faces, including both chamfered ends. All 49 engine thread tests, the UI dialog regression, and both SMLib guard checks pass. Independent Astra review found no correctness findings. Required cargo xtask ci passed repository validators and strict workspace Clippy; its full native test build was stopped because the available disk cannot hold the remaining binaries. All 14 selected crates pass cargo xtask wasm. The sweep group retains one documented baseline cylinder-section failure. Co-Authored-By: OpenAI Codex <noreply@openai.com> * Threads: a new Thread dialog leaves the default flank layout out of the command The Thread dialog put `continuous_flanks` into every new command, so the untouched dialog no longer sent only its face and `dialogs_thread::tests::every_option_reaches_the_command` failed. Continuous flanks are the command's default, so the dialog now sends the option only when the `thread_continuous_flanks` preference asks for the historical half-turn faces. Editing a saved thread still sends its saved layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Threads: continuous flanks are not a preference; record the ladder's thread cases New modeled threads always get continuous flank faces. The `thread_continuous_flanks` preference, its Design page row, its message catalog strings and its control-protocol entry are removed. A saved thread still keeps its own `continuous_flanks` field, so an old design reopens and rebuilds with its half-turn faces, and editing it keeps them. The dialog test now checks that a new Thread dialog leaves the field out and that reopening a saved thread keeps its layout. Kernel ladder: 29 cases make fewer faces, edges and vertices because each flank is one face: 06-threads/001 to 017 and 019 to 028, and 11-cumulative-parts/003 and 004. Their volume and area match the old golden to 4e-9 relative and every check passes. Run with `continuous_flanks: false`, all 29 make exactly the old counts. Only those 29 entries change; the ladder's order and report stay as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Benjamin Davis <benjamindavis@Benjamins-Mac-mini.local> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1033,6 +1033,10 @@ pub enum FeatureKind {
|
||||
length: Option<Expr>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
modeled: bool,
|
||||
/// Continuous flank faces for this feature. Missing in older designs: keep
|
||||
/// their half-turn faces and groove-before-chamfer order for saved references.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
continuous_flanks: bool,
|
||||
/// A point near the end of the face the thread starts at; `None`: the face's only free
|
||||
/// end (no material past it), or the end the pick at `face` lies near.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1412,6 +1412,7 @@ fn marking(p: Vec3) -> FeatureKind {
|
||||
chamfer_angle: None,
|
||||
chamfer_depth: None,
|
||||
steep_flank: Default::default(),
|
||||
continuous_flanks: false,
|
||||
old_auto_size: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,6 +765,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
designation,
|
||||
length,
|
||||
modeled,
|
||||
continuous_flanks,
|
||||
from,
|
||||
up_to,
|
||||
form,
|
||||
@@ -827,7 +828,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
None => note,
|
||||
});
|
||||
}
|
||||
if let Some(note) = cut(st, on, (*tip, angle, depth))? {
|
||||
if let Some(note) = cut_with_flanks(st, on, (*tip, angle, depth), *continuous_flanks)? {
|
||||
*warning = Some(match warning.take() {
|
||||
Some(w) => format!("{w}; {note}"),
|
||||
None => note,
|
||||
@@ -846,11 +847,18 @@ pub(super) fn runout(vals: &BTreeMap<String, Value>, e: &Option<crate::ThreadRun
|
||||
})
|
||||
}
|
||||
|
||||
/// Cut a modelled thread resolved on its face into its body: the grooves (SMLib's exact screw
|
||||
/// sweeps, a taper's crests by its envelope) by one B-rep boolean, then the chamfered tip
|
||||
/// (`tip`, with its angle and depth when given). What the user should be told about the result, if
|
||||
/// anything, is returned.
|
||||
/// Cut a modelled thread with its historical half-turn faces and groove-before-chamfer
|
||||
/// order. Existing hole features use this layout; Thread features keep their saved choice.
|
||||
pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (ThreadTip, Option<f64>, Option<f64>)) -> Result<Option<String>> {
|
||||
cut_with_flanks(st, on, (tip, angle, depth), false)
|
||||
}
|
||||
|
||||
fn cut_with_flanks(
|
||||
st: &mut ModelState,
|
||||
on: ThreadOn,
|
||||
(tip, angle, depth): (ThreadTip, Option<f64>, Option<f64>),
|
||||
continuous_flanks: bool,
|
||||
) -> Result<Option<String>> {
|
||||
let ThreadOn { info: t, cyl, ends, steep, ease, from: from_side, narrow, shoulder_unchecked } = on;
|
||||
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())) };
|
||||
@@ -888,11 +896,28 @@ pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (Threa
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
// Preserve historical construction order when rebuilding an older feature.
|
||||
let chamfer = |mut input: Traced| -> Result<Traced> {
|
||||
if tip != ThreadTip::None {
|
||||
for cone in tip_cones(&t, radius, ends, (tip, from_side), angle, depth, taper)? {
|
||||
let faces = cone.face_count();
|
||||
let cut =
|
||||
kernel::boolean(&input.body, &cone, BoolOp::Cut)?.ok_or_else(|| DocError::Invalid("the chamfer removed everything".into()))?;
|
||||
let unnamed = crate::lineage::Lineage::unnamed_faces(faces);
|
||||
let lineage = input.lineage.and_then(|l| crate::lineage::Lineage::step(&cut, &[&l, &unnamed]));
|
||||
input = Traced { body: cut, lineage };
|
||||
}
|
||||
}
|
||||
Ok(input)
|
||||
};
|
||||
if continuous_flanks {
|
||||
input = chamfer(input)?;
|
||||
}
|
||||
let cut_grooves = |ends: [End; 2]| -> Result<(Option<Body>, usize)> {
|
||||
let mut grooves = Vec::new();
|
||||
for g in thread_grooves(&t, groove_radius, (cyl.start, cyl.end), ends, steep, ease, taper)? {
|
||||
let section = Region2 { outer: solidcore_record::Loop2::polygon(&g.section), holes: Vec::new() };
|
||||
grooves.push(kernel::sweep_helix_radial(&g.plane, §ion, g.base, g.axis, g.pitch, g.turns, &g.law)?);
|
||||
grooves.push(kernel::sweep_helix_radial(&g.plane, §ion, g.base, g.axis, g.pitch, g.turns, &g.law, continuous_flanks)?);
|
||||
}
|
||||
if let Some(taper) = taper
|
||||
&& let Some(envelope) = taper_envelope(&t, radius, ends, taper)?
|
||||
@@ -932,16 +957,8 @@ pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (Threa
|
||||
// The groove's faces have no role to name them by.
|
||||
let groove = crate::lineage::Lineage::unnamed_faces(groove_faces);
|
||||
let lineage = input.lineage.and_then(|l| crate::lineage::Lineage::step(&nb, &[&l, &groove]));
|
||||
let mut nb = Traced { body: nb, lineage };
|
||||
if tip != ThreadTip::None {
|
||||
for cone in tip_cones(&t, radius, ends, (tip, from_side), angle, depth, taper)? {
|
||||
let faces = cone.face_count();
|
||||
let cut = kernel::boolean(&nb.body, &cone, BoolOp::Cut)?.ok_or_else(|| DocError::Invalid("the chamfer removed everything".into()))?;
|
||||
let unnamed = crate::lineage::Lineage::unnamed_faces(faces);
|
||||
let lineage = nb.lineage.and_then(|l| crate::lineage::Lineage::step(&cut, &[&l, &unnamed]));
|
||||
nb = Traced { body: cut, lineage };
|
||||
}
|
||||
}
|
||||
let nb = Traced { body: nb, lineage };
|
||||
let nb = if continuous_flanks { nb } else { chamfer(nb)? };
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
|
||||
}
|
||||
@@ -951,7 +968,7 @@ pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (Threa
|
||||
/// The chamfered tip of an external thread (ISO 4753, chamfered end CH) at the free ends `tip`
|
||||
/// names (`Start`: the end of the axis `from`, which must be free; `Both`: each free end):
|
||||
/// the material outside a cone `angle` to the axis (default 45°) that reaches the diameter
|
||||
/// d_f = d - 2 `depth` at the face's end, as a ring to cut after the groove, so the last teeth
|
||||
/// d_f = d - 2 `depth` at the face's end, as a ring to cut from the stock, so the last teeth
|
||||
/// taper into it. ISO 4753 bounds d_f by the external thread's minor diameter; the default is
|
||||
/// that bound, d3 = d - 1.226869 P (ISO 724, the rounded root of the design profile). It lies
|
||||
/// below the modelled basic root d1, so the cone crosses the root cylinder and leaves a whole
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
//! Saves each thread feature's continuous-flank layout choice. Older threads omit
|
||||
//! the field and keep their historical faces and chamfer order, preserving references.
|
||||
//! The previous release refuses designs with this newer format rather than losing it.
|
||||
@@ -2109,6 +2109,12 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
parameters.entry("convention").or_insert_with(|| json!(convention));
|
||||
}
|
||||
if let FeatureKind::Thread { continuous_flanks, .. } = &old.kind
|
||||
&& cid == "solid.thread"
|
||||
&& let Some(parameters) = params.as_object_mut()
|
||||
{
|
||||
parameters.entry("continuous_flanks").or_insert_with(|| json!(continuous_flanks));
|
||||
}
|
||||
// A thread saved before holes were sized by their nominal diameter keeps the size it had
|
||||
// when it is edited without naming one.
|
||||
if let FeatureKind::Thread { old_auto_size: true, .. } = &old.kind
|
||||
|
||||
@@ -163,7 +163,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|bspp|rp|bspt|npt (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); bspp \"G 1/2\" (ISO 228-1, 55° parallel); rp \"Rp 1/2\" (ISO 7-1's parallel internal thread, 55°, an R screws into it; internal only); bspt \"R 1/2\" external, \"Rc 1/2\" internal (ISO 7-1, 55°, taper 1 in 16); npt \"1/2-14 NPT\" (ASME B1.20.1, 60°, taper 1 in 16) — a taper thread starts at a free end (the pipe end or the hole mouth) with its standard diameter there, and an external one runs its standard length unless `length` or `up_to` says otherwise (default: the standard size that fits the face, a shaft its major diameter, a hole its nominal diameter (a 12 mm hole is M12) or else the size whose tap drill it is); hand?: right|left|both (default right; both: a right- and a left-handed groove crossing); old_auto_size?: bool (an automatic size keeps the rule of designs saved before a hole was sized by its nominal diameter: a 12 mm hole is M14; set on those threads, keeps them as they were when edited); 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_out?: expr|standard|short (modelled threads: the inner end, away from where the thread starts, fades out over this length (shorter than a turn too), its section moving out of the material as it turns; standard 2.5 P, short 1.25 P per ISO 3508; it lies past the full-form length, up to a shoulder's clearance; at a free end, inside the face; without it the groove stops square there, ending in its own profile; the start never fades: it runs off its free end, plain or chamfered (`tip`)); tip?: none|start|both (modelled external threads: start chamfers the end the thread starts from, which must be free, both each free end the thread reaches, 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 for a metric thread down to the external minor diameter d3 = d - 1.226869 P (ISO 724), the largest ISO 4753 allows; other forms give it); 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 hole as wide as the thread or wider is narrowed to the thread's minor diameter first, and the reply's warning says so; a shaft thicker than the thread is refused with the size to make it, never turned down"),
|
||||
.params("face: [x,y,z] on a cylindrical face; form?: metric|trapezoidal|acme|buttress|buttress_asme|bspp|rp|bspt|npt (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); bspp \"G 1/2\" (ISO 228-1, 55° parallel); rp \"Rp 1/2\" (ISO 7-1's parallel internal thread, 55°, an R screws into it; internal only); bspt \"R 1/2\" external, \"Rc 1/2\" internal (ISO 7-1, 55°, taper 1 in 16); npt \"1/2-14 NPT\" (ASME B1.20.1, 60°, taper 1 in 16) — a taper thread starts at a free end (the pipe end or the hole mouth) with its standard diameter there, and an external one runs its standard length unless `length` or `up_to` says otherwise (default: the standard size that fits the face, a shaft its major diameter, a hole its nominal diameter (a 12 mm hole is M12) or else the size whose tap drill it is); hand?: right|left|both (default right; both: a right- and a left-handed groove crossing); old_auto_size?: bool (an automatic size keeps the rule of designs saved before a hole was sized by its nominal diameter: a 12 mm hole is M14; set on those threads, keeps them as they were when edited); 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_out?: expr|standard|short (modelled threads: the inner end, away from where the thread starts, fades out over this length (shorter than a turn too), its section moving out of the material as it turns; standard 2.5 P, short 1.25 P per ISO 3508; it lies past the full-form length, up to a shoulder's clearance; at a free end, inside the face; without it the groove stops square there, ending in its own profile; the start never fades: it runs off its free end, plain or chamfered (`tip`)); tip?: none|start|both (modelled external threads: start chamfers the end the thread starts from, which must be free, both each free end the thread reaches, 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 for a metric thread down to the external minor diameter d3 = d - 1.226869 P (ISO 724), the largest ISO 4753 allows; other forms give it); 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); continuous_flanks?: bool (default true; false keeps the historical half-turn faces and groove-before-chamfer construction). 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 hole as wide as the thread or wider is narrowed to the thread's minor diameter first, and the reply's warning says so; 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")
|
||||
@@ -3884,11 +3884,17 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, msg));
|
||||
}
|
||||
let designation_is_none = designation.is_none();
|
||||
let continuous_flanks = match p.get("continuous_flanks") {
|
||||
None => true,
|
||||
Some(Value::Bool(value)) => *value,
|
||||
Some(_) => return Err(bad(cmd, "`continuous_flanks` must be a boolean")),
|
||||
};
|
||||
let kind = FeatureKind::Thread {
|
||||
face: FacePick(face),
|
||||
designation,
|
||||
length,
|
||||
modeled,
|
||||
continuous_flanks,
|
||||
from,
|
||||
up_to,
|
||||
form,
|
||||
|
||||
@@ -998,7 +998,7 @@ fn m10_stock(z0: f64, z1: f64, rmax: impl Fn(f64) -> f64) -> f64 {
|
||||
}
|
||||
|
||||
/// ISO 4753's chamfered end on both free ends of an M10 stud: a 45° cone down to the external
|
||||
/// minor diameter d3 (ISO 724), cut after the groove; the volume as the stock's chamfered section
|
||||
/// minor diameter d3 (ISO 724); the volume as the stock's chamfered section
|
||||
/// with the thread's material fraction (as ladder 06/006). d3 lies below the modelled root, so
|
||||
/// each end face is the whole disk of d3, with the cone a whole ring around it, not pinched
|
||||
/// where the groove reaches the end.
|
||||
@@ -1140,48 +1140,112 @@ fn a_runout_fades_the_groove_out_before_the_shoulder() {
|
||||
assert!(e.err().map(|e| e.to_string()).unwrap_or_default().contains("`ease_in` is gone"));
|
||||
}
|
||||
|
||||
/// A modelled M10 thread's flanks are several B-spline faces per turn (spans of one sweep); where
|
||||
/// two of them meet with one tangent plane and one curvature the join is left out of the shaded
|
||||
/// view (still pickable and in the wireframe), so the flank reads as one surface. The normal angle across each join is measured
|
||||
/// independently (`seam_diagnostics`, projecting stations onto both surfaces).
|
||||
#[test]
|
||||
fn a_modelled_threads_flank_joins_are_not_drawn() {
|
||||
fn a_saved_segmented_thread_flank_pick_does_not_select_a_different_face() {
|
||||
use solidcore_record::brep::Surface;
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 5.0, "height": 12, "body_name": "Shaft"}));
|
||||
run(&mut s, "solid.thread", json!({"face": [5.0, 0, 6.0], "designation": "M10x1.5", "modeled": true}));
|
||||
let st = s.model.state();
|
||||
let body = &st.body("Shaft").unwrap().body;
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let record = body.record();
|
||||
let free_form = |f: u32| {
|
||||
record
|
||||
.faces
|
||||
.get(f as usize)
|
||||
.is_some_and(|f| f.nurbs.is_none() && matches!(f.surface, Surface::Spline { .. } | Surface::RationalSpline { .. }))
|
||||
};
|
||||
let tolerances = solidcraft_kernel::ContinuityTolerances { gap_mm: 1e-3, angle_deg: 0.5, curvature_per_mm: 1.0, ..Default::default() };
|
||||
let report = solidcraft_kernel::seam_diagnostics(body, &[], 33, tolerances).unwrap();
|
||||
let mut flank_joins = 0;
|
||||
let mut worst: f64 = 0.0;
|
||||
for seam in &report.seams {
|
||||
let e = seam.edge;
|
||||
let uses = &record.edges[e].uses;
|
||||
if uses.len() != 2 || !uses.iter().all(|u| free_form(u.face)) {
|
||||
continue;
|
||||
|
||||
// Captured from the segmented build before continuous_flanks existed, including the old
|
||||
// groove-before-chamfer construction. Picks near both chamfered ends exercise its order.
|
||||
let cases = [
|
||||
("none", vec![("F2:face:12", Vec3::new(4.7828951862188696, -0.0057530612776590315, 1.6606274291037717))]),
|
||||
(
|
||||
"both",
|
||||
vec![
|
||||
("F2:face:41", Vec3::new(0.09242260275006192, 4.273234209721337, 0.23586874398281996)),
|
||||
("F2:face:7", Vec3::new(0.9625365002593353, -4.6849682261915895, 6.262443362555148)),
|
||||
("F2:face:51", Vec3::new(-0.0924226027501412, 4.273234209721336, 11.764131256017183)),
|
||||
],
|
||||
),
|
||||
];
|
||||
for (tip, picks) in cases {
|
||||
let mut thread = json!({
|
||||
"designation": "M10x1.5", "face": [5, 0, 6], "face_names": ["F1:side"],
|
||||
"id": 2, "modeled": true, "name": "Thread1", "suppressed": false, "type": "thread"
|
||||
});
|
||||
if tip != "none" {
|
||||
thread["tip"] = json!(tip);
|
||||
}
|
||||
let angle = seam.normal_angle_deg.value;
|
||||
if angle < 0.5 {
|
||||
flank_joins += 1;
|
||||
worst = worst.max(angle);
|
||||
assert!(mesh.smooth_edges[e] && !mesh.shaded_edge_visible(e), "join {e} at {angle} deg is drawn");
|
||||
assert!(mesh.edge_visible(e) && mesh.edge_pickable(e), "join {e} stays in the wireframe and pickable");
|
||||
} else {
|
||||
assert!(mesh.shaded_edge_visible(e), "crease {e} at {angle} deg is hidden");
|
||||
let mut saved: Value = serde_json::from_str(&solidcraft_doc::Document::default().to_json()).unwrap();
|
||||
saved["features"] = json!([
|
||||
{
|
||||
"axis": [0, 0, 1], "base": [0, 0, 0], "body_names": ["Shaft"], "height": "12",
|
||||
"id": 1, "name": "Cylinder1", "operation": "new_body", "param_names": ["d1", "d2"],
|
||||
"radius": "5", "suppressed": false, "type": "cylinder"
|
||||
}, thread
|
||||
]);
|
||||
saved["next_id"] = json!(3);
|
||||
saved["params"] = json!([]);
|
||||
let mut reopened = Session::new(solidcraft_doc::Document::from_json(&saved.to_string()).unwrap());
|
||||
let check_picks = |session: &Session| {
|
||||
let rebuilt = session.model.state();
|
||||
let shaft = rebuilt.body("Shaft").unwrap();
|
||||
for (name, pick) in &picks {
|
||||
let (face, point) = solidcraft_doc::naming::face_on_body(shaft, name, *pick).unwrap();
|
||||
assert!(point.dist(*pick) < 0.01, "{tip}: {name} must stay on its original flank: {point:?}");
|
||||
assert!(
|
||||
matches!(shaft.body.record().faces[face].surface, Surface::Spline { .. } | Surface::RationalSpline { .. }),
|
||||
"{tip}: {name} must not select a cylinder"
|
||||
);
|
||||
}
|
||||
};
|
||||
check_picks(&reopened);
|
||||
run(
|
||||
&mut reopened,
|
||||
"timeline.redefine",
|
||||
json!({
|
||||
"feature": 2, "command": "solid.thread",
|
||||
"params": {"face": [5, 0, 6], "designation": "M10x1.5", "modeled": true, "tip": tip}
|
||||
}),
|
||||
);
|
||||
assert!(matches!(reopened.doc.feature(2).unwrap().kind, solidcraft_doc::FeatureKind::Thread { continuous_flanks: false, .. }));
|
||||
check_picks(&reopened);
|
||||
let saved_again = solidcraft_doc::Document::from_json(&reopened.doc.to_json()).unwrap();
|
||||
assert!(matches!(saved_again.feature(2).unwrap().kind, solidcraft_doc::FeatureKind::Thread { continuous_flanks: false, .. }));
|
||||
}
|
||||
}
|
||||
|
||||
/// A modelled M10 thread keeps each flank as one B-spline face across all turns. Cylinder seams
|
||||
/// must not split either flank into separate faces for selection, meshing or STEP export.
|
||||
#[test]
|
||||
fn a_modelled_thread_keeps_each_flank_as_one_face() {
|
||||
use solidcore_record::brep::Surface;
|
||||
// Include a partial final turn and a chamfered stock: neither may fragment the flanks.
|
||||
for (height, tip) in [(12.0, "none"), (12.345, "none"), (12.345, "both")] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.cylinder", json!({"radius": 5.0, "height": height, "body_name": "Shaft"}));
|
||||
run(&mut s, "solid.thread", json!({"face": [5.0, 0, height / 2.0], "designation": "M10x1.5", "modeled": true, "tip": tip}));
|
||||
let st = s.model.state();
|
||||
let shaft = st.body("Shaft").unwrap();
|
||||
let body = &shaft.body;
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let record = body.record();
|
||||
let flanks: Vec<usize> = record
|
||||
.faces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| f.nurbs.is_none() && matches!(f.surface, Surface::Spline { .. } | Surface::RationalSpline { .. }))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
assert_eq!(flanks.len(), 2, "one selectable face per flank, independent of the turn count");
|
||||
let names = solidcraft_doc::naming::face_names(shaft);
|
||||
let picks: Vec<_> = flanks
|
||||
.iter()
|
||||
.map(|&flank| {
|
||||
assert!(mesh.tri_face.iter().any(|f| *f as usize == flank), "flank {flank} must tessellate");
|
||||
(names[flank].clone(), solidcraft_doc::naming::face_spot(shaft, flank).unwrap().point)
|
||||
})
|
||||
.collect();
|
||||
// The named picks downstream face commands store must still find the same kind of face
|
||||
// after the history is serialized and rebuilt, including the reordered chamfer cuts.
|
||||
let reopened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
let rebuilt = reopened.model.state();
|
||||
let shaft = rebuilt.body("Shaft").unwrap();
|
||||
for (name, point) in picks {
|
||||
let (face, found) = solidcraft_doc::naming::face_on_body(shaft, &name, point).unwrap();
|
||||
assert!(found.dist(point) < 1e-6, "the saved flank pick must remain on its named face: {name}");
|
||||
assert!(matches!(shaft.body.record().faces[face].surface, Surface::Spline { .. } | Surface::RationalSpline { .. }));
|
||||
}
|
||||
}
|
||||
// SMLib's flank pieces meet with normals about 1.5e-6 degrees apart (measured).
|
||||
assert!(flank_joins >= 32 && worst < 1e-4, "{flank_joins} flank joins, the worst {worst} deg apart");
|
||||
}
|
||||
|
||||
/// A pipe thread's profile for [`pipe_material`]: (flank half angle, thread height h, flat at crest
|
||||
|
||||
@@ -25,7 +25,8 @@ pub fn sweep_helix(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec
|
||||
/// [`sweep_helix`] with the profile moved off the axis as it turns: at turn t it first moves
|
||||
/// `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.
|
||||
/// groove fading in or out over a runout: SMLib's radial screw sweep. `continuous_flanks`
|
||||
/// joins the spline walls across cylinder seams; false retains the historical faces.
|
||||
pub fn sweep_helix_radial(
|
||||
plane: &Plane,
|
||||
region: &Region2,
|
||||
@@ -34,18 +35,16 @@ pub fn sweep_helix_radial(
|
||||
pitch: f64,
|
||||
turns: f64,
|
||||
law: &[(f64, f64)],
|
||||
continuous_flanks: bool,
|
||||
) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_helix_radial");
|
||||
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))
|
||||
{
|
||||
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)
|
||||
crate::smlib_sweeps::screw_radial(plane, &crate::smlib_sweeps::pieces([&profile]), axis_origin, k, pitch, turns, law, continuous_flanks)
|
||||
}
|
||||
|
||||
/// A helical sweep's axis and profile (the region's outer loop), checked: finite turns and
|
||||
|
||||
@@ -1837,11 +1837,14 @@ mod imp {
|
||||
pitch: f64,
|
||||
turns: f64,
|
||||
law: &[(f64, f64)],
|
||||
continuous_flanks: bool,
|
||||
) -> Result<Body> {
|
||||
let c = curves(plane, pieces)?;
|
||||
let tolerance = (pitch.abs() * 1e-4).clamp(1e-6, 1e-2);
|
||||
let options =
|
||||
solidcraft_smlib::sweeps::ScrewSweepOptions { law: (!law.is_empty()).then_some(law), join_spline_walls: continuous_flanks, tolerance };
|
||||
let brep =
|
||||
Brep::screw_sweep_radial(&c, p(origin), p(axis), pitch, turns, law, tolerance).map_err(|e| failed("screw sweep with a radial law", e))?;
|
||||
Brep::screw_sweep_with_options(&c, p(origin), p(axis), pitch, turns, options).map_err(|e| failed("screw sweep with a radial law", e))?;
|
||||
let body = body_of(brep, "screw sweep")?;
|
||||
let roles = roles(&body, plane, None, &seeds(plane, pieces, |q| q), &[], true)?;
|
||||
with_history(body, &roles, pieces)
|
||||
|
||||
@@ -1467,20 +1467,30 @@ fn cylinder_piece(k: &mut Kernel, b: &mut Brep, m: &Screw, a: Point3, e: Point3,
|
||||
})
|
||||
}
|
||||
|
||||
/// `screw_wall`: the wall one profile curve sweeps over `st`: across the curve its NURBS, across
|
||||
/// the turns each control point's path, the weights the curve's times the arcs'.
|
||||
fn screw_wall(m: &Screw, c: &CurveCanonical, st: &Stations) -> Out<Surface> {
|
||||
let across = 2 * st.spans() + 1;
|
||||
/// `screw_wall`: the wall one profile curve sweeps over consecutive station pieces: across the
|
||||
/// curve its NURBS, across the turns each control point's path. Cylinder seams split the pieces,
|
||||
/// not this wall: join their existing nets at the shared endpoint, without refitting the spans.
|
||||
fn screw_wall(m: &Screw, c: &CurveCanonical, pieces: &[Stations]) -> Out<Surface> {
|
||||
let across = 2 * pieces.iter().map(Stations::spans).sum::<usize>() + 1;
|
||||
let mut net = Vec::with_capacity(c.control_points.len() * across);
|
||||
let mut weights = Vec::with_capacity(net.capacity());
|
||||
for (l, &p) in c.control_points.iter().enumerate() {
|
||||
let w = c.weights.get(l).copied().unwrap_or(1.0);
|
||||
for (j, q) in screw_path(m, p, st).into_iter().enumerate() {
|
||||
net.push(q);
|
||||
weights.push(w * st.weight(j));
|
||||
for (i, st) in pieces.iter().enumerate() {
|
||||
for (j, q) in screw_path(m, p, st).into_iter().enumerate().skip(usize::from(i > 0)) {
|
||||
net.push(q);
|
||||
weights.push(w * st.weight(j));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (vknots, vmult) = st.knots();
|
||||
let vknots: Vec<f64> = pieces.iter().enumerate().flat_map(|(i, st)| st.t.iter().copied().skip(usize::from(i > 0))).collect();
|
||||
let mut vmult = vec![2; vknots.len()];
|
||||
if let Some(first) = vmult.first_mut() {
|
||||
*first = 3;
|
||||
}
|
||||
if let Some(last) = vmult.last_mut() {
|
||||
*last = 3;
|
||||
}
|
||||
let data = CanonicalSurfaceData {
|
||||
u_degree: c.degree,
|
||||
v_degree: 2,
|
||||
@@ -1531,7 +1541,7 @@ fn screw_placed(m: &Screw, c: &Profile, a: f64) -> Profile {
|
||||
/// by span (SM_API's curve sweep carries the profile on its own frame, which along a helix turns
|
||||
/// it away from the screw motion), lines parallel to the axis as exact cylinder pieces, the caps
|
||||
/// where the motion starts and ends, all stitched and oriented.
|
||||
fn screw(k: &mut Kernel, profile: &[Profile], mut m: Screw, turns: f64, tolerance: f64) -> solidcore::Result<Out<Brep>> {
|
||||
fn screw(k: &mut Kernel, profile: &[Profile], mut m: Screw, turns: f64, tolerance: f64, join_spline_walls: bool) -> solidcore::Result<Out<Brep>> {
|
||||
let curves = match parse(k, profile) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
@@ -1576,7 +1586,8 @@ fn screw(k: &mut Kernel, profile: &[Profile], mut m: Screw, turns: f64, toleranc
|
||||
}
|
||||
}
|
||||
// Walls of lines parallel to the axis are exact cylinders, which cannot turn on themselves:
|
||||
// then every wall comes in pieces of half a turn (the last up to an eighth longer).
|
||||
// their faces come in pieces of half a turn (the last up to an eighth longer). All walls use
|
||||
// these stations so their shared boundary curves agree, but spline walls keep one face.
|
||||
let mut radius = Vec::with_capacity(curves.len());
|
||||
for c in &curves {
|
||||
radius.push(axial_radius(k, &m, c, tolerance)?);
|
||||
@@ -1622,17 +1633,22 @@ fn screw(k: &mut Kernel, profile: &[Profile], mut m: Screw, turns: f64, toleranc
|
||||
pieces.push(Stations::new(from, to, n as usize));
|
||||
}
|
||||
let mut b = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
for st in &pieces {
|
||||
// A radial law's knots can change the wall's tangent, so retain those face boundaries. Half
|
||||
// turns are only cylinder seams: join the other walls' spans when explicitly requested.
|
||||
for group in pieces.chunk_by(|a, _| join_spline_walls && !m.angles.contains(&a.last())) {
|
||||
for (i, (c, wall)) in curves.iter().zip(&walls).enumerate() {
|
||||
let r = radius.get(i).copied().unwrap_or(0.0);
|
||||
if r > 0.0 && m.offset(st.first()) == m.offset(st.last()) {
|
||||
let (Some(first), Some(last)) = (group.first(), group.last()) else { continue };
|
||||
if r > 0.0 && m.offset(first.first()) == m.offset(last.last()) {
|
||||
let (a, e) = ends(k, c)?;
|
||||
if let Err(err) = cylinder_piece(k, &mut b, &m, a, e, r + m.offset(st.first()), st)? {
|
||||
return Ok(Err(err));
|
||||
for st in group {
|
||||
if let Err(err) = cylinder_piece(k, &mut b, &m, a, e, r + m.offset(st.first()), st)? {
|
||||
return Ok(Err(err));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let surface = match screw_wall(&m, wall, st) {
|
||||
let surface = match screw_wall(&m, wall, group) {
|
||||
Ok(s) => s,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
@@ -1671,6 +1687,7 @@ pub(crate) fn screw_sweep(
|
||||
turns: f64,
|
||||
law: Option<&[(f64, f64)]>,
|
||||
tolerance: f64,
|
||||
join_spline_walls: bool,
|
||||
) -> Result<Body> {
|
||||
if !screw_ok(origin, pitch, turns, tolerance) {
|
||||
return Err(Error::Argument);
|
||||
@@ -1698,7 +1715,7 @@ pub(crate) fn screw_sweep(
|
||||
angles: law.iter().map(|(t, _)| TAU * t).collect(),
|
||||
offsets: law.iter().map(|(_, o)| *o).collect(),
|
||||
};
|
||||
screw(k, profile, m, turns, tolerance)
|
||||
screw(k, profile, m, turns, tolerance, join_spline_walls)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -299,8 +299,8 @@ fn on_core(op: &Op) -> crate::Result<Body> {
|
||||
Op::Loft(s, d, cap) => loft(s, *d, *cap),
|
||||
Op::LoftToPoint(s, apex) => loft_to_point(s, *apex),
|
||||
Op::Stations(p, s, e) => sweep_stations(p, s, *e),
|
||||
Op::Screw(p, o, a, pitch, turns, tol) => screw_sweep(p, *o, *a, *pitch, *turns, None, *tol),
|
||||
Op::ScrewRadial(p, o, a, pitch, turns, law, tol) => screw_sweep(p, *o, *a, *pitch, *turns, Some(law), *tol),
|
||||
Op::Screw(p, o, a, pitch, turns, tol) => screw_sweep(p, *o, *a, *pitch, *turns, None, *tol, false),
|
||||
Op::ScrewRadial(p, o, a, pitch, turns, law, tol) => screw_sweep(p, *o, *a, *pitch, *turns, Some(law), *tol, false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,10 +610,14 @@ fn hostile_arguments_are_refused() {
|
||||
let flat = Affine { m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], t: [0.0; 3] };
|
||||
assert_eq!(sweep_stations(&sq, &[translate([0.0; 3]), flat], Ends::Capped).err(), Some(Error::Argument), "a singular map");
|
||||
assert_eq!(sweep_stations(&sq, &[translate([0.0; 3])], Ends::Capped).err(), Some(Error::Argument), "one station");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 0.0, 1.0, None, 1e-3).err(), Some(Error::Argument), "no pitch");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 300.0, None, 1e-3).err(), Some(Error::Argument), "too many turns");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 1.0, Some(&[(0.5, 0.0), (0.2, 0.0)]), 1e-3).err(), Some(Error::Argument), "a law going back");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 1.0, Some(&[]), 1e-3).err(), Some(Error::Argument), "an empty law");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 0.0, 1.0, None, 1e-3, false).err(), Some(Error::Argument), "no pitch");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 300.0, None, 1e-3, false).err(), Some(Error::Argument), "too many turns");
|
||||
assert_eq!(
|
||||
screw_sweep(&sq, [0.0; 3], Z, 1.0, 1.0, Some(&[(0.5, 0.0), (0.2, 0.0)]), 1e-3, false).err(),
|
||||
Some(Error::Argument),
|
||||
"a law going back"
|
||||
);
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 1.0, Some(&[]), 1e-3, false).err(), Some(Error::Argument), "an empty law");
|
||||
let b = make_box([0.0; 3], [1.0; 3]).unwrap();
|
||||
assert_eq!(project_curves(&b, [0.5; 3], 0.01, &sq, [0.0; 3], 1e-6).err(), Some(Error::Argument), "no direction");
|
||||
assert_eq!(project_curves(&b, [0.5; 3], 0.0, &sq, Z, 1e-6).err(), Some(Error::Argument), "no pick tolerance");
|
||||
|
||||
@@ -11,6 +11,17 @@ use serde_json::json;
|
||||
use crate::trace;
|
||||
use crate::{Brep, Result, core};
|
||||
|
||||
/// Screw-sweep topology and its approximation tolerance.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ScrewSweepOptions<'a> {
|
||||
/// Optional radial law as `(turn, offset)` pairs.
|
||||
pub law: Option<&'a [(f64, f64)]>,
|
||||
/// Join spline walls across cylinder seams; false retains the historical faces.
|
||||
pub join_spline_walls: bool,
|
||||
/// Maximum wall approximation error, mm.
|
||||
pub tolerance: f64,
|
||||
}
|
||||
|
||||
/// A 3D curve of a profile, path or section.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Curve {
|
||||
@@ -247,7 +258,7 @@ impl Brep {
|
||||
/// `turns` right-handed turns about it, rising `pitch` (signed) per turn, capped at both ends,
|
||||
/// its walls within about `tolerance` of the true screw surfaces.
|
||||
pub fn screw_sweep(profile: &[Curve], origin: [f64; 3], direction: [f64; 3], pitch: f64, turns: f64, tolerance: f64) -> Result<Brep> {
|
||||
let r = core::sweep::screw_sweep(profile, origin, direction, pitch, turns, None, tolerance).map(Brep::made_core);
|
||||
let r = core::sweep::screw_sweep(profile, origin, direction, pitch, turns, None, tolerance, false).map(Brep::made_core);
|
||||
trace::made(
|
||||
"screw_sweep",
|
||||
&[],
|
||||
@@ -272,13 +283,35 @@ impl Brep {
|
||||
law: &[(f64, f64)],
|
||||
tolerance: f64,
|
||||
) -> Result<Brep> {
|
||||
let r = core::sweep::screw_sweep(profile, origin, direction, pitch, turns, Some(law), tolerance).map(Brep::made_core);
|
||||
let r = core::sweep::screw_sweep(profile, origin, direction, pitch, turns, Some(law), tolerance, false).map(Brep::made_core);
|
||||
trace::made("screw_sweep", &[], &r, || {
|
||||
json!({"profile": trace::curves(profile), "origin": origin, "direction": direction, "pitch": pitch, "turns": turns,
|
||||
"law": law, "tolerance": tolerance})
|
||||
});
|
||||
r
|
||||
}
|
||||
|
||||
/// A screw sweep with explicit face layout. The historical entry points retain
|
||||
/// half-turn faces; new thread features request continuous spline walls here.
|
||||
pub fn screw_sweep_with_options(
|
||||
profile: &[Curve],
|
||||
origin: [f64; 3],
|
||||
direction: [f64; 3],
|
||||
pitch: f64,
|
||||
turns: f64,
|
||||
options: ScrewSweepOptions<'_>,
|
||||
) -> Result<Brep> {
|
||||
let r = core::sweep::screw_sweep(profile, origin, direction, pitch, turns, options.law, options.tolerance, options.join_spline_walls)
|
||||
.map(Brep::made_core);
|
||||
trace::made("screw_sweep", &[], &r, || {
|
||||
json!({
|
||||
"profile": trace::curves(profile), "origin": origin, "direction": direction,
|
||||
"pitch": pitch, "turns": turns, "law": options.law,
|
||||
"tolerance": options.tolerance, "join_spline_walls": options.join_spline_walls,
|
||||
})
|
||||
});
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/// A sweep frame's name in a trace.
|
||||
|
||||
@@ -96,9 +96,41 @@ fn sweeps_lofts_and_screws() {
|
||||
let tooth = polygon(&[[4.0, 0.0, 0.0], [4.4, 0.0, 0.0], [4.4, 0.0, 0.4], [4.0, 0.0, 0.4]]);
|
||||
let b = Brep::screw_sweep(&tooth, [0.0; 3], [0.0, 0.0, 1.0], 1.0, 3.0, 1e-4).unwrap();
|
||||
assert_close(solid_volume(&b, "screw"), 0.16 * TAU * 4.2 * 3.0, 1e-4, "screw sweep");
|
||||
let historical = b.to_native().unwrap();
|
||||
assert!(historical.faces.iter().filter(|f| matches!(f.surface, solidcore_record::brep::Surface::RationalSpline { .. })).count() > 2);
|
||||
let options = crate::sweeps::ScrewSweepOptions { law: None, join_spline_walls: true, tolerance: 1e-4 };
|
||||
let b = Brep::screw_sweep_with_options(&tooth, [0.0; 3], [0.0, 0.0, 1.0], 1.0, 3.0, options).unwrap();
|
||||
let record = b.to_native().unwrap();
|
||||
let flanks = record.faces.iter().filter(|f| matches!(f.surface, solidcore_record::brep::Surface::RationalSpline { .. })).count();
|
||||
assert_eq!(flanks, 2, "each screw flank must remain one face across the cylinder seams");
|
||||
// The tooth moved out 1 mm over its three turns, linearly: A 2 pi (c turns + int offset dt).
|
||||
let b = Brep::screw_sweep_radial(&tooth, [0.0; 3], [0.0, 0.0, 1.0], 1.0, 3.0, &[(0.0, 0.0), (3.0, 1.0)], 1e-4).unwrap();
|
||||
let b = Brep::screw_sweep_with_options(
|
||||
&tooth,
|
||||
[0.0; 3],
|
||||
[0.0, 0.0, 1.0],
|
||||
1.0,
|
||||
3.0,
|
||||
crate::sweeps::ScrewSweepOptions { law: Some(&[(0.0, 0.0), (3.0, 1.0)]), join_spline_walls: true, tolerance: 1e-4 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_close(solid_volume(&b, "radial screw"), 0.16 * TAU * (4.2 * 3.0 + 1.5), 1e-4, "radial screw sweep");
|
||||
assert_eq!(b.to_native().unwrap().faces.len(), 6, "a linear radial law needs four continuous walls and two caps");
|
||||
// Two constant intervals around a sloping interval, with a partial last turn: keep the
|
||||
// law's corners as face boundaries, while joining the half-turn pieces within each interval.
|
||||
let law = [(0.0, 0.0), (0.65, 0.0), (1.9, 0.4), (3.2, 0.4)];
|
||||
let b = Brep::screw_sweep_with_options(
|
||||
&tooth,
|
||||
[0.0; 3],
|
||||
[0.0, 0.0, 1.0],
|
||||
1.0,
|
||||
3.2,
|
||||
crate::sweeps::ScrewSweepOptions { law: Some(&law), join_spline_walls: true, tolerance: 1e-4 },
|
||||
)
|
||||
.unwrap();
|
||||
assert!(b.check(CheckLevel::All).unwrap().is_valid());
|
||||
let record = b.to_native().unwrap();
|
||||
let splines = record.faces.iter().filter(|f| matches!(f.surface, solidcore_record::brep::Surface::RationalSpline { .. })).count();
|
||||
assert_eq!(splines, 8, "two flanks across three law intervals and two sloping radial walls");
|
||||
assert_eq!(Brep::screw_sweep_radial(&tooth, [0.0; 3], [0.0, 0.0, 1.0], 1.0, 3.0, &[(2.0, 0.0), (1.0, 1.0)], 1e-4).err(), Some(Error::Argument));
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ const NO_KERNEL: [&str; 14] = [
|
||||
"hold_a_kernel",
|
||||
];
|
||||
|
||||
const ANSWERING: [&str; 63] = [
|
||||
const ANSWERING: [&str; 64] = [
|
||||
"set_dump_dir",
|
||||
"current_call",
|
||||
"crash_test",
|
||||
@@ -159,6 +159,7 @@ const ANSWERING: [&str; 63] = [
|
||||
"loft_to_point",
|
||||
"screw_sweep",
|
||||
"screw_sweep_radial",
|
||||
"screw_sweep_with_options",
|
||||
];
|
||||
|
||||
/// Every `pub fn` of the files that hold the crate's kernel entry points is accounted for.
|
||||
@@ -278,6 +279,17 @@ fn every_entry_point_answers() {
|
||||
says("loft_to_point", Brep::loft_to_point(&lines, p(5.0, 5.0, 12.0)));
|
||||
says("screw_sweep", Brep::screw_sweep(&lines, p(30.0, 0.0, 0.0), p(0.0, 0.0, 1.0), 12.0, 1.0, 0.01));
|
||||
says("screw_sweep_radial", Brep::screw_sweep_radial(&lines, p(30.0, 0.0, 0.0), p(0.0, 0.0, 1.0), 12.0, 1.0, &[(0.0, 0.0), (1.0, 0.0)], 0.01));
|
||||
says(
|
||||
"screw_sweep_with_options",
|
||||
Brep::screw_sweep_with_options(
|
||||
&lines,
|
||||
p(30.0, 0.0, 0.0),
|
||||
p(0.0, 0.0, 1.0),
|
||||
12.0,
|
||||
1.0,
|
||||
crate::sweeps::ScrewSweepOptions { law: None, join_spline_walls: true, tolerance: 0.01 },
|
||||
),
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -658,6 +658,7 @@ pub(crate) fn reopen(d: &mut Dialog, s: &Session, kind: &FeatureKind) {
|
||||
designation,
|
||||
length,
|
||||
modeled,
|
||||
continuous_flanks,
|
||||
from,
|
||||
up_to,
|
||||
form,
|
||||
@@ -674,6 +675,7 @@ pub(crate) fn reopen(d: &mut Dialog, s: &Session, kind: &FeatureKind) {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
d.extra.insert("continuous_flanks".into(), json!(continuous_flanks));
|
||||
if *modeled {
|
||||
d.extra.insert("modeled".into(), json!(true));
|
||||
}
|
||||
|
||||
@@ -20,6 +20,23 @@ fn shaft() -> Harness {
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_thread_leaves_the_flank_layout_out_and_an_edit_keeps_its_saved_layout() {
|
||||
for continuous in [false, true] {
|
||||
let mut h = shaft();
|
||||
h.app.session.selection = vec![thread_face(&h.app, "Body1")];
|
||||
h.app.dialog = crate::dialogs::Dialog::for_command(&h.app, "solid.thread");
|
||||
// Continuous flanks are the command's default, so a new thread's dialog never sends the field.
|
||||
assert_eq!(params(&h).get("continuous_flanks"), None);
|
||||
// A saved thread (an old design's has the historical layout) keeps its layout when edited.
|
||||
h.app.run("solid.thread", json!({"face": [4,0,5], "designation": "M8", "continuous_flanks": continuous})).unwrap();
|
||||
let saved = h.app.session.doc.features.last().unwrap().kind.clone();
|
||||
let mut d = crate::dialogs::Dialog::for_command(&h.app, "solid.thread").unwrap();
|
||||
crate::dialogs_thread::reopen(&mut d, &h.app.session, &saved);
|
||||
assert_eq!(d.extra["continuous_flanks"], json!(continuous));
|
||||
}
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness, handles: &[&str]) {
|
||||
for handle in handles {
|
||||
h.step(&json!({"click":{"handle":handle}})).unwrap_or_else(|e| panic!("{handle}: {e}"));
|
||||
|
||||
@@ -842,206 +842,206 @@
|
||||
},
|
||||
"ladder/06-threads/001-external-m10-full": {
|
||||
"measured": {
|
||||
"area_mm2": 1528.998556041668,
|
||||
"area_mm2": 1528.9985557240047,
|
||||
"body_count": 1,
|
||||
"edges": 252,
|
||||
"faces": 86,
|
||||
"vertices": 168,
|
||||
"volume_mm3": 1954.2451742203707
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 1954.2451742206745
|
||||
},
|
||||
"seconds": 2.721541155,
|
||||
"seconds": 2.413831875,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/002-external-m10x1-fine": {
|
||||
"measured": {
|
||||
"area_mm2": 1581.5205133736267,
|
||||
"area_mm2": 1581.520513204513,
|
||||
"body_count": 1,
|
||||
"edges": 372,
|
||||
"faces": 126,
|
||||
"vertices": 248,
|
||||
"volume_mm3": 2081.900137123529
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 2081.900137123524
|
||||
},
|
||||
"seconds": 4.221791527,
|
||||
"seconds": 3.841230667,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/003-external-m10-partial-length": {
|
||||
"measured": {
|
||||
"area_mm2": 1314.9630323937222,
|
||||
"area_mm2": 1314.9630320406159,
|
||||
"body_count": 1,
|
||||
"edges": 132,
|
||||
"faces": 46,
|
||||
"vertices": 88,
|
||||
"volume_mm3": 2155.219832193732
|
||||
"edges": 15,
|
||||
"faces": 7,
|
||||
"vertices": 10,
|
||||
"volume_mm3": 2155.219832193756
|
||||
},
|
||||
"seconds": 1.339277799,
|
||||
"seconds": 1.17349675,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/004-external-m10-to-shoulder": {
|
||||
"measured": {
|
||||
"area_mm2": 2070.374916609214,
|
||||
"area_mm2": 2070.374919605752,
|
||||
"body_count": 1,
|
||||
"edges": 219,
|
||||
"faces": 76,
|
||||
"vertices": 146,
|
||||
"volume_mm3": 3639.157043325919
|
||||
"edges": 15,
|
||||
"faces": 8,
|
||||
"vertices": 10,
|
||||
"volume_mm3": 3639.157100211207
|
||||
},
|
||||
"seconds": 5.739657892,
|
||||
"seconds": 4.374615583,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/005-internal-m10-through": {
|
||||
"measured": {
|
||||
"area_mm2": 4978.101312125217,
|
||||
"area_mm2": 4978.101307811231,
|
||||
"body_count": 1,
|
||||
"edges": 186,
|
||||
"faces": 64,
|
||||
"vertices": 124,
|
||||
"volume_mm3": 16688.181679696947
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 16688.181647442983
|
||||
},
|
||||
"seconds": 1.790578757,
|
||||
"seconds": 1.7246769579999999,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/006-external-m10-into-chamfer": {
|
||||
"measured": {
|
||||
"area_mm2": 1508.1088885319687,
|
||||
"area_mm2": 1508.1088881036242,
|
||||
"body_count": 1,
|
||||
"edges": 252,
|
||||
"faces": 86,
|
||||
"vertices": 168,
|
||||
"volume_mm3": 1948.692674104833
|
||||
"edges": 15,
|
||||
"faces": 7,
|
||||
"vertices": 10,
|
||||
"volume_mm3": 1948.692674863475
|
||||
},
|
||||
"seconds": 2.752528111,
|
||||
"seconds": 2.476385042,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/007-external-m10-at-filleted-shoulder": {
|
||||
"measured": {
|
||||
"area_mm2": 2042.128006779366,
|
||||
"area_mm2": 2042.1280065080136,
|
||||
"body_count": 1,
|
||||
"edges": 214,
|
||||
"faces": 75,
|
||||
"vertices": 142,
|
||||
"volume_mm3": 3659.598361255103
|
||||
"edges": 19,
|
||||
"faces": 10,
|
||||
"vertices": 12,
|
||||
"volume_mm3": 3659.59835997161
|
||||
},
|
||||
"seconds": 5.496053349,
|
||||
"seconds": 4.258890708,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/008-multi-start-thread": {
|
||||
"measured": {
|
||||
"area_mm2": 1532.2853636908305,
|
||||
"area_mm2": 1532.2853637719477,
|
||||
"body_count": 1,
|
||||
"edges": 264,
|
||||
"faces": 90,
|
||||
"vertices": 176,
|
||||
"volume_mm3": 1954.2451868020048
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 1954.2451868023975
|
||||
},
|
||||
"seconds": 4.422846562,
|
||||
"seconds": 4.592702792,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/009-thread-runout-length": {
|
||||
"measured": {
|
||||
"area_mm2": 2023.0586519925544,
|
||||
"area_mm2": 2023.0586555266757,
|
||||
"body_count": 1,
|
||||
"edges": 214,
|
||||
"faces": 77,
|
||||
"vertices": 140,
|
||||
"volume_mm3": 3673.4420040320556
|
||||
"edges": 22,
|
||||
"faces": 11,
|
||||
"vertices": 14,
|
||||
"volume_mm3": 3673.4420109650064
|
||||
},
|
||||
"seconds": 2.142433107,
|
||||
"seconds": 1.888353333,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/010-part-threaded-boss": {
|
||||
"measured": {
|
||||
"area_mm2": 13370.25800424205,
|
||||
"area_mm2": 13370.258004400335,
|
||||
"body_count": 1,
|
||||
"edges": 120,
|
||||
"faces": 44,
|
||||
"vertices": 80,
|
||||
"volume_mm3": 53181.215541289486
|
||||
"edges": 57,
|
||||
"faces": 23,
|
||||
"vertices": 38,
|
||||
"volume_mm3": 53181.21553952572
|
||||
},
|
||||
"seconds": 2.7871945240000002,
|
||||
"seconds": 1.5094906670000001,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/011-external-tr20x4": {
|
||||
"measured": {
|
||||
"area_mm2": 3008.510981839494,
|
||||
"area_mm2": 3008.5109816059444,
|
||||
"body_count": 1,
|
||||
"edges": 84,
|
||||
"faces": 30,
|
||||
"vertices": 56,
|
||||
"volume_mm3": 6065.464857365654
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 6065.4648573656405
|
||||
},
|
||||
"seconds": 0.947265504,
|
||||
"seconds": 0.854130584,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/012-external-acme-3-4-6": {
|
||||
"measured": {
|
||||
"area_mm2": 2943.7315978027445,
|
||||
"area_mm2": 2943.7315975079605,
|
||||
"body_count": 1,
|
||||
"edges": 84,
|
||||
"faces": 30,
|
||||
"vertices": 56,
|
||||
"volume_mm3": 5689.271631870986
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 5689.271631871066
|
||||
},
|
||||
"seconds": 0.917790907,
|
||||
"seconds": 0.823329166,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/013-external-s20x4": {
|
||||
"measured": {
|
||||
"area_mm2": 3376.925744829369,
|
||||
"area_mm2": 3376.925747037271,
|
||||
"body_count": 1,
|
||||
"edges": 87,
|
||||
"faces": 31,
|
||||
"vertices": 58,
|
||||
"volume_mm3": 5454.578669308655
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 5454.578689688301
|
||||
},
|
||||
"seconds": 0.956965336,
|
||||
"seconds": 0.846362209,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/014-external-buttress-asme-1.5-8": {
|
||||
"measured": {
|
||||
"area_mm2": 5916.276938815345,
|
||||
"area_mm2": 5916.2769386008,
|
||||
"body_count": 1,
|
||||
"edges": 87,
|
||||
"faces": 31,
|
||||
"vertices": 58,
|
||||
"volume_mm3": 19631.08121588802
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 19631.081217292656
|
||||
},
|
||||
"seconds": 1.097856354,
|
||||
"seconds": 0.994141625,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/015-internal-tr20x4-nut": {
|
||||
"measured": {
|
||||
"area_mm2": 9076.623628232444,
|
||||
"area_mm2": 9076.623627903613,
|
||||
"body_count": 1,
|
||||
"edges": 96,
|
||||
"faces": 34,
|
||||
"vertices": 64,
|
||||
"volume_mm3": 32097.52027153327
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 32097.52027153298
|
||||
},
|
||||
"seconds": 1.073692761,
|
||||
"seconds": 0.838320083,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/016-external-m10-left-hand": {
|
||||
"measured": {
|
||||
"area_mm2": 1528.9985552508233,
|
||||
"area_mm2": 1528.9985553876666,
|
||||
"body_count": 1,
|
||||
"edges": 252,
|
||||
"faces": 86,
|
||||
"vertices": 168,
|
||||
"volume_mm3": 1954.245174273949
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 1954.2451742731246
|
||||
},
|
||||
"seconds": 2.586554869,
|
||||
"seconds": 2.466066459,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/017-external-m10-chamfered-tip": {
|
||||
"measured": {
|
||||
"area_mm2": 1492.9858794840054,
|
||||
"area_mm2": 1492.9858800784912,
|
||||
"body_count": 1,
|
||||
"edges": 258,
|
||||
"faces": 88,
|
||||
"vertices": 172,
|
||||
"volume_mm3": 1945.3545045351243
|
||||
"edges": 18,
|
||||
"faces": 8,
|
||||
"vertices": 12,
|
||||
"volume_mm3": 1945.3545006540319
|
||||
},
|
||||
"seconds": 3.390192017,
|
||||
"seconds": 2.624632958,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/018-external-m10-both-hands": {
|
||||
@@ -1058,122 +1058,122 @@
|
||||
},
|
||||
"ladder/06-threads/019-external-tr40x14-p7-two-start": {
|
||||
"measured": {
|
||||
"area_mm2": 11049.102021460496,
|
||||
"area_mm2": 11049.102023505651,
|
||||
"body_count": 1,
|
||||
"edges": 96,
|
||||
"faces": 34,
|
||||
"vertices": 64,
|
||||
"volume_mm3": 43534.48234540685
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 43534.48234540645
|
||||
},
|
||||
"seconds": 1.6360441730000002,
|
||||
"seconds": 1.363557083,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/020-external-g-1-2": {
|
||||
"measured": {
|
||||
"area_mm2": 1819.8329651487302,
|
||||
"area_mm2": 1819.8329649987736,
|
||||
"body_count": 1,
|
||||
"edges": 84,
|
||||
"faces": 30,
|
||||
"vertices": 56,
|
||||
"volume_mm3": 3355.9227118042977
|
||||
"edges": 12,
|
||||
"faces": 6,
|
||||
"vertices": 8,
|
||||
"volume_mm3": 3355.9227118042204
|
||||
},
|
||||
"seconds": 0.998855354,
|
||||
"seconds": 0.864747709,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/021-internal-rp-1-2": {
|
||||
"measured": {
|
||||
"area_mm2": 5528.39986772297,
|
||||
"area_mm2": 5528.399867787501,
|
||||
"body_count": 1,
|
||||
"edges": 96,
|
||||
"faces": 34,
|
||||
"vertices": 64,
|
||||
"volume_mm3": 14061.220081960184
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 14061.2200819604
|
||||
},
|
||||
"seconds": 0.995023297,
|
||||
"seconds": 0.881482,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/022-external-r-1-2-taper": {
|
||||
"measured": {
|
||||
"area_mm2": 2892.9309285104473,
|
||||
"area_mm2": 2892.9309274100933,
|
||||
"body_count": 1,
|
||||
"edges": 122,
|
||||
"faces": 52,
|
||||
"vertices": 72,
|
||||
"volume_mm3": 8233.025967958223
|
||||
"edges": 21,
|
||||
"faces": 9,
|
||||
"vertices": 14,
|
||||
"volume_mm3": 8233.025961932732
|
||||
},
|
||||
"seconds": 1.682378267,
|
||||
"seconds": 1.451540541,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/023-external-1-2-npt": {
|
||||
"measured": {
|
||||
"area_mm2": 3023.0574345312343,
|
||||
"area_mm2": 3023.0574417375406,
|
||||
"body_count": 1,
|
||||
"edges": 119,
|
||||
"faces": 51,
|
||||
"vertices": 70,
|
||||
"volume_mm3": 8226.404184276684
|
||||
"edges": 18,
|
||||
"faces": 8,
|
||||
"vertices": 12,
|
||||
"volume_mm3": 8226.404180678332
|
||||
},
|
||||
"seconds": 1.8495682,
|
||||
"seconds": 1.530033542,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/024-internal-rc-1-2-taper": {
|
||||
"measured": {
|
||||
"area_mm2": 6149.480584083193,
|
||||
"area_mm2": 6149.480575315043,
|
||||
"body_count": 1,
|
||||
"edges": 130,
|
||||
"faces": 55,
|
||||
"vertices": 77,
|
||||
"volume_mm3": 17217.301948494354
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 17217.301966031755
|
||||
},
|
||||
"seconds": 1.553609743,
|
||||
"seconds": 1.298487167,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/025-external-s20x4-steep-flank-end-to-shoulder": {
|
||||
"measured": {
|
||||
"area_mm2": 6009.162011983694,
|
||||
"area_mm2": 6009.162011728351,
|
||||
"body_count": 1,
|
||||
"edges": 108,
|
||||
"faces": 39,
|
||||
"vertices": 72,
|
||||
"volume_mm3": 13886.806724574824
|
||||
"edges": 15,
|
||||
"faces": 8,
|
||||
"vertices": 10,
|
||||
"volume_mm3": 13886.806738072268
|
||||
},
|
||||
"seconds": 3.113976484,
|
||||
"seconds": 2.193244958,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/026-external-s20x4-steep-flank-start-to-shoulder": {
|
||||
"measured": {
|
||||
"area_mm2": 6009.162011488332,
|
||||
"area_mm2": 6009.162011409378,
|
||||
"body_count": 1,
|
||||
"edges": 108,
|
||||
"faces": 39,
|
||||
"vertices": 72,
|
||||
"volume_mm3": 13886.806673641227
|
||||
"edges": 15,
|
||||
"faces": 8,
|
||||
"vertices": 10,
|
||||
"volume_mm3": 13886.806675087246
|
||||
},
|
||||
"seconds": 3.47044052,
|
||||
"seconds": 2.2872160409999998,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/027-external-m10-chamfered-start": {
|
||||
"measured": {
|
||||
"area_mm2": 1510.9922178981603,
|
||||
"area_mm2": 1510.9922171362944,
|
||||
"body_count": 1,
|
||||
"edges": 255,
|
||||
"faces": 87,
|
||||
"vertices": 170,
|
||||
"volume_mm3": 1949.7998386132565
|
||||
"edges": 15,
|
||||
"faces": 7,
|
||||
"vertices": 10,
|
||||
"volume_mm3": 1949.7998326210782
|
||||
},
|
||||
"seconds": 3.106665118,
|
||||
"seconds": 2.556222625,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/028-external-tr20x4-chamfer-30-deg-2.5-deep": {
|
||||
"measured": {
|
||||
"area_mm2": 2626.5765791797803,
|
||||
"area_mm2": 2626.5765787039873,
|
||||
"body_count": 1,
|
||||
"edges": 90,
|
||||
"faces": 32,
|
||||
"vertices": 60,
|
||||
"volume_mm3": 5769.128682105137
|
||||
"edges": 18,
|
||||
"faces": 8,
|
||||
"vertices": 12,
|
||||
"volume_mm3": 5769.12868113977
|
||||
},
|
||||
"seconds": 1.350682371,
|
||||
"seconds": 1.026389666,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/06-threads/029-external-tr20x4-chamfer-needs-depth": {
|
||||
@@ -1810,26 +1810,26 @@
|
||||
},
|
||||
"ladder/11-cumulative-parts/003-threaded-flange": {
|
||||
"measured": {
|
||||
"area_mm2": 17528.149663462264,
|
||||
"area_mm2": 17528.14966260621,
|
||||
"body_count": 1,
|
||||
"edges": 112,
|
||||
"faces": 41,
|
||||
"vertices": 74,
|
||||
"volume_mm3": 52806.59825789034
|
||||
"edges": 52,
|
||||
"faces": 21,
|
||||
"vertices": 34,
|
||||
"volume_mm3": 52806.59825993053
|
||||
},
|
||||
"seconds": 7.150208538,
|
||||
"seconds": 3.593792417,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/11-cumulative-parts/004-housing": {
|
||||
"measured": {
|
||||
"area_mm2": 21156.778657103445,
|
||||
"area_mm2": 21156.778656747345,
|
||||
"body_count": 1,
|
||||
"edges": 218,
|
||||
"faces": 81,
|
||||
"vertices": 144,
|
||||
"volume_mm3": 15111.545095719644
|
||||
"edges": 164,
|
||||
"faces": 63,
|
||||
"vertices": 108,
|
||||
"volume_mm3": 15111.54509757647
|
||||
},
|
||||
"seconds": 4.128625132,
|
||||
"seconds": 3.651175791,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/12-sheet-metal/001-base-flange": {
|
||||
@@ -2001,5 +2001,5 @@
|
||||
"status": "pass"
|
||||
}
|
||||
},
|
||||
"reason": "a modelled external thread running a whole face to a shoulder now runs on through the plane of the face end and is trimmed there (#11610): 06-threads/004, 007, 010, 025, 026 and 11-cumulative-parts/003 gain the faces of the trimmed last turn, their volumes worked out again by hand; every other case as before. Before: 04-spline-profiles/009 and 012 re-recorded, an extruded spline or ellipse sweeps one wall (one-curve extrude): 009 makes 3 faces, 3 edges and 2 vertices (8, 18 and 12 per span before); 012 makes 28 faces, 78 edges and 52 vertices (31, 86 and 57 before); every other case as before. Before: recorded at solidcore 8ddce9016: 06-threads/018 makes 606 edges and 404 vertices (486 and 324 at 97ed188db), its 204 faces, area and volume the same to 1e-11; every other case as before. Before: recorded on solidcore, the only kernel since the C++ SMLib kernel was removed (the golden before was C++ SMLib v1.1.0-alpha, a18bbe5): 07-fillets/012 passes (the variable round no longer gains the End at the smooth vertex: 8 faces, 18 edges, 12 vertices, 47755.12 mm3); 07-fillets/021 has 24 edges and 15 vertices (no End cap edge); 07-fillets/013 removes 47325.53 mm3; 06-threads/018 makes 486 edges and 324 vertices every run, so it is no longer marked unstable"
|
||||
"reason": "new modelled threads keep each thread flank as one continuous face, without the half-turn splits (#11737): 06-threads/001 to 017 and 019 to 028, and 11-cumulative-parts/003 and 004, make fewer faces, edges and vertices; their volume and area are the same to 4e-9 relative and every check passes, and with continuous_flanks false all 29 make their old counts exactly; 06-threads/018 (both hands) keeps its faces, which the crossing grooves split anyway; every other case as before. Before: a modelled external thread running a whole face to a shoulder now runs on through the plane of the face end and is trimmed there (#11610): 06-threads/004, 007, 010, 025, 026 and 11-cumulative-parts/003 gain the faces of the trimmed last turn, their volumes worked out again by hand; every other case as before. Before: 04-spline-profiles/009 and 012 re-recorded, an extruded spline or ellipse sweeps one wall (one-curve extrude): 009 makes 3 faces, 3 edges and 2 vertices (8, 18 and 12 per span before); 012 makes 28 faces, 78 edges and 52 vertices (31, 86 and 57 before); every other case as before. Before: recorded at solidcore 8ddce9016: 06-threads/018 makes 606 edges and 404 vertices (486 and 324 at 97ed188db), its 204 faces, area and volume the same to 1e-11; every other case as before. Before: recorded on solidcore, the only kernel since the C++ SMLib kernel was removed (the golden before was C++ SMLib v1.1.0-alpha, a18bbe5): 07-fillets/012 passes (the variable round no longer gains the End at the smooth vertex: 8 faces, 18 edges, 12 vertices, 47755.12 mm3); 07-fillets/021 has 24 edges and 15 vertices (no End cap edge); 07-fillets/013 removes 47325.53 mm3; 06-threads/018 makes 486 edges and 324 vertices every run, so it is no longer marked unstable"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user