mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
Tests: the thread whose display is checked is made as the thread feature makes one, with exact helices
thread_display_tests sewed its M10 and M12 threads from cubic Bezier pieces approximating
helices, which lie about 1e-5 mm off the core and crest cylinders. No exact (same-parameter)
pcurve exists on the cylinder for such an edge, and SolidCraft's pcurves are exact B-splines
only. Sewing refused the core face ("no exact pcurve of a B-spline on a spline surface"). Truck
had fitted one. The thread feature makes its helices exactly, with SMLib's screw sweep, so the
test now does the same: the ISO ridge's section is screw-swept and joined to the core cylinder.
The section reaches a tenth of a pitch into the core along its flanks. ridge_volume is still the
thread's exact volume, and both display tests run unchanged, from the body as made and after a
STEP round trip. Imported edges that lie off their surfaces stay refused; they are Repair Part's
job.
The quality check's sag is now measured at the surface point nearest each triangle's centre.
Before, it was read at the mean of the corners' parameters. A closed surface's parameters are
kept within one period, so a triangle across the core's seam had corners a period apart, and
their mean read 6.27 mm off a triangle that lies on the surface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,114 +4,31 @@ use solidcraft_geom::tess::TessPlan;
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
|
||||
use std::f64::consts::{PI, TAU};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Body, DISPLAY_ANGLE, EdgeSpec, FaceSpec, SurfSpec, sew};
|
||||
use crate::{Body, DISPLAY_ANGLE};
|
||||
|
||||
/// A helix about Z of radius `r` from angle `a0` to `a1`, rising `pitch` per turn from `z0` at
|
||||
/// angle 0, as cubic Bézier pieces of at most `TAU / 16`.
|
||||
fn helix(r: f64, z0: f64, pitch: f64, a0: f64, a1: f64) -> EdgeSpec {
|
||||
let n = (((a1 - a0).abs() / (TAU / 16.0)).ceil() as usize).max(1);
|
||||
let h = (a1 - a0) / n as f64;
|
||||
let k = 4.0 / 3.0 * (h / 4.0).tan();
|
||||
let at = |a: f64| Vec3::new(r * a.cos(), r * a.sin(), z0 + pitch * a / TAU);
|
||||
let rise = Vec3::new(0.0, 0.0, pitch * h / TAU / 3.0);
|
||||
let turn = |a: f64| Vec3::new(-a.sin(), a.cos(), 0.0) * (r * k);
|
||||
let mut ctrl = vec![at(a0)];
|
||||
for i in 0..n {
|
||||
let (s, e) = (a0 + h * i as f64, a0 + h * (i + 1) as f64);
|
||||
ctrl.extend([at(s) + turn(s) + rise, at(e) - turn(e) - rise, at(e)]);
|
||||
}
|
||||
EdgeSpec::Chain { degree: 3, ctrl: ctrl.into() }
|
||||
}
|
||||
|
||||
fn rev(e: &EdgeSpec) -> EdgeSpec {
|
||||
match e {
|
||||
EdgeSpec::Chain { degree, ctrl } => EdgeSpec::Chain { degree: *degree, ctrl: ctrl.iter().rev().copied().collect::<Arc<[Vec3]>>() },
|
||||
EdgeSpec::Line { a, b } => EdgeSpec::Line { a: *b, b: *a },
|
||||
EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: *b, b: *a, mid: *mid },
|
||||
}
|
||||
}
|
||||
|
||||
fn line(a: Vec3, b: Vec3) -> EdgeSpec {
|
||||
EdgeSpec::Line { a, b }
|
||||
}
|
||||
|
||||
fn ends(e: &EdgeSpec) -> (Vec3, Vec3) {
|
||||
match e {
|
||||
EdgeSpec::Chain { ctrl, .. } => (ctrl[0], ctrl[ctrl.len() - 1]),
|
||||
EdgeSpec::Line { a, b } | EdgeSpec::Arc { a, b, .. } => (*a, *b),
|
||||
}
|
||||
}
|
||||
|
||||
/// A full circle about Z at height `z`, as two arcs.
|
||||
fn circle(r: f64, z: f64) -> Vec<EdgeSpec> {
|
||||
let p = |a: f64| Vec3::new(r * a.cos(), r * a.sin(), z);
|
||||
vec![EdgeSpec::Arc { a: p(0.3), b: p(0.3 + PI), mid: p(0.3 + PI / 2.0) }, EdgeSpec::Arc { a: p(0.3 + PI), b: p(0.3), mid: p(0.3 + 1.5 * PI) }]
|
||||
}
|
||||
|
||||
/// A threaded shaft the way a modelled thread usually comes out of a CAD system: an ISO metric
|
||||
/// ridge (60° flanks, P/8 crest) of `turns` turns wound on a core of the minor diameter, the
|
||||
/// flanks ruled B-spline surfaces between helices, the crest and the core cylinders trimmed by
|
||||
/// those helices.
|
||||
/// A threaded shaft the way the thread feature makes one: an ISO metric ridge (60° flanks, P/8
|
||||
/// crest) of `turns` turns wound on a core of the minor diameter, the ridge SMLib's screw sweep
|
||||
/// of its section (exact helices, the flanks and crest its screw walls, flat ends in the planes
|
||||
/// through the axis where it starts and stops) joined to the core cylinder, which the flanks then
|
||||
/// trim along helices. The section reaches a tenth of a pitch into the core along its flanks, so
|
||||
/// the join cuts it at the core rather than meeting it face to face.
|
||||
fn ridge_thread(major: f64, pitch: f64, turns: f64) -> Body {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let h = pitch * 3f64.sqrt() / 2.0;
|
||||
let (rc, rm) = (major / 2.0 - 5.0 * h / 8.0, major / 2.0);
|
||||
let (foot, crest) = ((pitch - pitch / 4.0) / 2.0, pitch / 16.0);
|
||||
let z0 = pitch;
|
||||
let len = z0 + pitch * turns + pitch;
|
||||
let end = TAU * turns;
|
||||
let fl = helix(rc, z0 - foot, pitch, 0.0, end);
|
||||
let fu = helix(rc, z0 + foot, pitch, 0.0, end);
|
||||
let cl = helix(rm, z0 - crest, pitch, 0.0, end);
|
||||
let cu = helix(rm, z0 + crest, pitch, 0.0, end);
|
||||
let (fl0, fl1) = ends(&fl);
|
||||
let (fu0, fu1) = ends(&fu);
|
||||
let (cl0, cl1) = ends(&cl);
|
||||
let (cu0, cu1) = ends(&cu);
|
||||
let at = |r: f64, a: f64, z: f64| Vec3::new(r * a.cos(), r * a.sin(), z);
|
||||
let mid = z0 + pitch / 2.0;
|
||||
let out = |a: f64| Vec3::new(a.cos(), a.sin(), 0.0);
|
||||
let flank = 0.5f64;
|
||||
let faces = vec![
|
||||
FaceSpec { loops: vec![circle(rc, 0.0).iter().rev().map(rev).collect()], surface: SurfSpec::Plane, probe: None },
|
||||
FaceSpec { loops: vec![circle(rc, len)], surface: SurfSpec::Plane, probe: None },
|
||||
FaceSpec {
|
||||
loops: vec![circle(rc, 0.0), circle(rc, len).iter().rev().map(rev).collect(), vec![line(fl0, fu0), fu.clone(), line(fu1, fl1), rev(&fl)]],
|
||||
surface: SurfSpec::Revolved {
|
||||
a: Vec3::new(rc, 0.0, -1.0),
|
||||
b: Vec3::new(rc, 0.0, len + 1.0),
|
||||
axis_origin: Vec3::ZERO,
|
||||
axis: Vec3::Z,
|
||||
angle: TAU,
|
||||
},
|
||||
probe: Some((at(rc, PI, z0 * 0.5), out(PI))),
|
||||
},
|
||||
FaceSpec {
|
||||
loops: vec![vec![fl.clone(), line(fl1, cl1), rev(&cl), line(cl0, fl0)]],
|
||||
surface: SurfSpec::Ruled { a: fl.clone(), b: cl.clone() },
|
||||
probe: Some((at((rc + rm) / 2.0, PI, mid - (foot + crest) / 2.0), out(PI) * flank - Vec3::Z * 0.866)),
|
||||
},
|
||||
FaceSpec {
|
||||
loops: vec![vec![cu.clone(), line(cu1, fu1), rev(&fu), line(fu0, cu0)]],
|
||||
surface: SurfSpec::Ruled { a: fu.clone(), b: cu.clone() },
|
||||
probe: Some((at((rc + rm) / 2.0, PI, mid + (foot + crest) / 2.0), out(PI) * flank + Vec3::Z * 0.866)),
|
||||
},
|
||||
FaceSpec {
|
||||
loops: vec![vec![cl.clone(), line(cl1, cu1), rev(&cu), line(cu0, cl0)]],
|
||||
surface: SurfSpec::Revolved {
|
||||
a: Vec3::new(rm, 0.0, -1.0),
|
||||
b: Vec3::new(rm, 0.0, len + 1.0),
|
||||
axis_origin: Vec3::ZERO,
|
||||
axis: Vec3::Z,
|
||||
angle: TAU,
|
||||
},
|
||||
probe: Some((at(rm, PI, mid), out(PI))),
|
||||
},
|
||||
FaceSpec { loops: vec![vec![line(fl0, cl0), line(cl0, cu0), line(cu0, fu0), line(fu0, fl0)]], surface: SurfSpec::Plane, probe: None },
|
||||
FaceSpec { loops: vec![vec![line(fl1, fu1), line(fu1, cu1), line(cu1, cl1), line(cl1, fl1)]], surface: SurfSpec::Plane, probe: None },
|
||||
];
|
||||
sew(&faces).unwrap()
|
||||
// The ridge's half width at radius r, along its flanks.
|
||||
let half = |r: f64| foot + (crest - foot) * (r - rc) / (rm - rc);
|
||||
let r0 = rc - 0.1 * pitch;
|
||||
let section = Loop2::polygon(&[Vec2::new(r0, -half(r0)), Vec2::new(rm, -crest), Vec2::new(rm, crest), Vec2::new(r0, half(r0))]);
|
||||
let plane = Plane { origin: Vec3::new(0.0, 0.0, z0), x: Vec3::X, y: Vec3::Z };
|
||||
let ridge = crate::sweep_helix(&plane, &Region2 { outer: section, holes: Vec::new() }, Vec3::ZERO, Vec3::Z, pitch, turns).unwrap();
|
||||
let core = Region2 { outer: Loop2::circle(Vec2::ZERO, rc), holes: Vec::new() };
|
||||
let core = crate::extrude(&Plane::XY, &[core], 0.0, len).unwrap().pop().unwrap();
|
||||
crate::boolean(&core, &ridge, crate::BoolOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn tol(b: &Body) -> f64 {
|
||||
@@ -127,8 +44,10 @@ struct Quality {
|
||||
}
|
||||
|
||||
/// Triangles thinner than 1e-3 of their longest side; triangles whose winding disagrees with
|
||||
/// their vertex normals; the largest gap between a triangle's centre and the surface at the
|
||||
/// centre's parameters.
|
||||
/// their vertex normals; the largest gap between a triangle's centre and the surface, at the
|
||||
/// surface point nearest the centre (a few Newton steps from a corner's parameters: a closed
|
||||
/// surface's parameters are kept within one period, so a triangle across its seam has corners
|
||||
/// whose parameters lie a period apart, and their mean is nowhere near the triangle).
|
||||
fn quality(plan: &TessPlan, mesh: &Mesh) -> Quality {
|
||||
let mut q = Quality { tris: mesh.triangles.len(), slivers: 0, flipped: 0, worst_sag: 0.0, min_angle: 180.0 };
|
||||
for t in &mesh.triangles {
|
||||
@@ -155,8 +74,19 @@ fn quality(plan: &TessPlan, mesh: &Mesh) -> Quality {
|
||||
let Some(s) = f.surface.and_then(|i| plan.surfaces.get(i as usize)) else { continue };
|
||||
let pos = |k: u32| f.fixed.get(k as usize).copied().unwrap_or_else(|| s.eval(f.uv[k as usize][0], f.uv[k as usize][1]).0);
|
||||
for t in &f.triangles {
|
||||
let uv = t.iter().fold([0.0, 0.0], |m, &k| [m[0] + f.uv[k as usize][0] / 3.0, m[1] + f.uv[k as usize][1] / 3.0]);
|
||||
let mid = (pos(t[0]) + pos(t[1]) + pos(t[2])) * (1.0 / 3.0);
|
||||
let mut uv = f.uv[t[0] as usize];
|
||||
for _ in 0..8 {
|
||||
let Some((p, su, sv)) = s.derivs(uv[0], uv[1]) else { break };
|
||||
let d = mid - p;
|
||||
let (a, b, c) = (su.dot(su), su.dot(sv), sv.dot(sv));
|
||||
let det = a * c - b * b;
|
||||
if det.abs() < 1e-300 {
|
||||
break;
|
||||
}
|
||||
let (x, y) = (su.dot(d), sv.dot(d));
|
||||
uv = [uv[0] + (c * x - b * y) / det, uv[1] + (a * y - b * x) / det];
|
||||
}
|
||||
let (on, n) = s.eval(uv[0], uv[1]);
|
||||
let sag = (on - mid).dot(n).abs();
|
||||
q.worst_sag = q.worst_sag.max(sag);
|
||||
@@ -193,9 +123,9 @@ fn threads() -> Vec<(String, Body, f64, f64, f64)> {
|
||||
}
|
||||
|
||||
/// Issue 1289: a modelled thread's faces turn several times around the shaft. The core and the
|
||||
/// crest cylinders, trimmed by helices (and the core bounded by two circles without a seam
|
||||
/// edge), had no display plan, and the reference mesher drew them with triangles across the
|
||||
/// solid. They are now cut along a seam: the display mesh stays on the surfaces.
|
||||
/// crest cylinders, trimmed by helices, had no display plan, and the reference mesher drew them
|
||||
/// with triangles across the solid. They are now cut along a seam: the display mesh stays on
|
||||
/// the surfaces.
|
||||
#[test]
|
||||
fn thread_display_mesh_stays_on_its_surfaces() {
|
||||
for (name, b, d, p, turns) in threads() {
|
||||
|
||||
Reference in New Issue
Block a user