diff --git a/crates/kernel/src/thread_display_tests.rs b/crates/kernel/src/thread_display_tests.rs index 40a845d67a..63f5f3cf88 100644 --- a/crates/kernel/src/thread_display_tests.rs +++ b/crates/kernel/src/thread_display_tests.rs @@ -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::>() }, - 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 { - 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() {