diff --git a/crates/kernel/src/boolean_backend.rs b/crates/kernel/src/boolean_backend.rs index 9372e968ef..4b2ad5beab 100644 --- a/crates/kernel/src/boolean_backend.rs +++ b/crates/kernel/src/boolean_backend.rs @@ -76,7 +76,18 @@ mod smlib { let Some(r) = ta.boolean(&tb, kind).map_err(|e| failed("operation", e))? else { return Ok(None) }; let check = r.check(CheckLevel::All).map_err(|e| failed("check", e))?; if !check.is_valid() { - return Err(failed("SMLib's check refuses the result", check)); + // Whether an operand failed it already says whose the fault is. + let operands: Vec<&str> = [("the first operand", &sa), ("the second operand", &sb)] + .into_iter() + .filter(|(_, s)| s.check(CheckLevel::All).is_ok_and(|c| !c.is_valid())) + .map(|(name, _)| name) + .collect(); + let what = if operands.is_empty() { + "SMLib's check refuses the result".to_string() + } else { + format!("SMLib's check refuses the result ({} already fails it)", operands.join(" and ")) + }; + return Err(failed(&what, check)); } let (body, or) = body_from_smlib_ordered(&r)?; let history = match (oa, ob, or) { diff --git a/crates/smlib/src/import.rs b/crates/smlib/src/import.rs index 1a2a97425c..3ca34de3d7 100644 --- a/crates/smlib/src/import.rs +++ b/crates/smlib/src/import.rs @@ -388,23 +388,31 @@ fn net_rows(a: &mut Arrays, cps: impl Iterator) { net(a, cps); } -/// A plane `origin + u·U + v·V`. With unit, orthogonal axes it is SMLib's plane, whose (u, v) are -/// distances along them; otherwise it is the exact bilinear patch over the domain. +/// A plane `origin + u·U + v·V` as SMLib's plane, whose (u, v) are distances along unit, +/// orthogonal axes: U's direction, and the one at a right angle to it in the plane, the normal +/// U × V as the record's. The range is the record's domain mapped onto those axes (its bounding +/// box: a face's range need not be tight, and SMLib rebuilds an analytic face's trims from its +/// edges). Truck's planes run through three points, their axes the face's own sides. fn plane(a: &mut Arrays, origin: [f64; 3], u: [f64; 3], v: [f64; 3], domain: [[f64; 2]; 2]) -> R<()> { let [[u0, u1], [v0, v1]] = domain; - let unit = |x: [f64; 3]| (dot(x, x).sqrt() - 1.0).abs() <= 1e-12; - if unit(u) && unit(v) && dot(u, v).abs() <= 1e-12 { - a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_PLANE)); - a.r3(flat::FACE_PLANE_ORIGIN, origin); - a.r3(flat::FACE_PLANE_AXIS, cross(u, v)); - a.r3(flat::FACE_PLANE_REF_DIRECTION, u); - a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); - return Ok(()); + let n = cross(u, v); + let (lu, ln) = (dot(u, u).sqrt(), dot(n, n).sqrt()); + if !(lu > 0.0 && ln > 0.0 && lu.is_finite() && ln.is_finite()) { + return Err(Unsupported::Invalid("plane axes")); } - nurbs_face(a, &[u0, u0, u1, u1], &[v0, v0, v1, v1], 2, 2)?; - let at = |s: f64, t: f64| add(origin, add(scale(u, s), scale(v, t))); - net(a, [at(u0, v0), at(u0, v1), at(u1, v0), at(u1, v1)].into_iter().map(|p| (p, 1.0))); - a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); + let (x, z) = (scale(u, 1.0 / lu), scale(n, 1.0 / ln)); + let y = cross(z, x); + let corners = [[u0, v0], [u0, v1], [u1, v0], [u1, v1]].map(|[s, t]| { + let d = add(scale(u, s), scale(v, t)); + [dot(d, x), dot(d, y)] + }); + let lo = |k: usize| corners.iter().map(|c| c[k]).fold(f64::INFINITY, f64::min); + let hi = |k: usize| corners.iter().map(|c| c[k]).fold(f64::NEG_INFINITY, f64::max); + a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_PLANE)); + a.r3(flat::FACE_PLANE_ORIGIN, origin); + a.r3(flat::FACE_PLANE_AXIS, z); + a.r3(flat::FACE_PLANE_REF_DIRECTION, x); + a.r(flat::FACE_RANGE).extend([lo(0), lo(1), hi(0), hi(1)]); Ok(()) } diff --git a/crates/smlib/src/import/revolved.rs b/crates/smlib/src/import/revolved.rs index 917a00cf76..6ed7df7180 100644 --- a/crates/smlib/src/import/revolved.rs +++ b/crates/smlib/src/import/revolved.rs @@ -32,13 +32,18 @@ pub(super) fn analytic(brep: &b::Brep) -> R> { for (i, f) in out.faces.iter_mut().enumerate() { let b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } = &f.surface else { continue }; let rev = Revolution::new(profile, *origin, *axis, matrix, *forward)?; - let (surface, mut domain, v) = rev.analytic(f.domain)?; - // Angles start within the first turn, where SMLib's degree domains are at home. - domain[0] = first_turn(domain[0]); + let (mut surface, mut domain, v) = rev.analytic(f.domain)?; + // The angle about the axis starts at 0: the frame turns to the face's first angle (SMLib + // refuses an analytic range past 360°). A torus's tube angle can't turn so: it starts + // within the first turn, where SMLib's degree domains are at home. + let start = domain[0][0]; + turn_frame(&mut surface, start); + domain[0] = [0.0, domain[0][1] - start]; if matches!(surface, b::Surface::Torus { .. }) { domain[1] = first_turn(domain[1]); } - let flip = agree(&f.surface, f.domain, &surface, domain, &rev, &v, brep.tolerance.max(1e-9 * size))?; + let map = Map { rev: &rev, start, v: &v }; + let flip = agree((&f.surface, f.domain), (&surface, domain), map, brep.tolerance.max(1e-9 * size))?; f.surface = surface; f.domain = domain; if flip { @@ -51,6 +56,18 @@ pub(super) fn analytic(brep: &b::Brep) -> R> { Ok(Cow::Owned(out)) } +/// An analytic surface's frame turned by `angle` about its axis: u then counts from there. +fn turn_frame(s: &mut b::Surface, angle: f64) { + if let b::Surface::Cylinder { frame, .. } | b::Surface::Cone { frame, .. } | b::Surface::Sphere { frame, .. } | b::Surface::Torus { frame, .. } = + s + { + let (x, y) = (v3(frame.x_axis), v3(frame.y_axis)); + let (sin, cos) = angle.sin_cos(); + frame.x_axis = arr(x * cos + y * sin); + frame.y_axis = arr(y * cos - x * sin); + } +} + /// An angle range moved by whole turns to start in [0, 2π). fn first_turn([a, z]: [f64; 2]) -> [f64; 2] { let shift = (a / TAU).floor() * TAU; @@ -243,13 +260,21 @@ impl Revolution { /// The analytic (u, v) of the record's (u, v): the angle is the revolution's own, v that of /// the profile's point (at angle 0), unwrapped to within half a turn of `near`. - fn map(&self, v: &V, uv: [f64; 2], near: f64) -> Option<[f64; 2]> { + fn map(&self, v: &V, uv: [f64; 2], start: f64, near: f64) -> Option<[f64; 2]> { let (t, angle) = if self.forward { (uv[0], uv[1]) } else { (uv[1], uv[0]) }; let p = self.profile.point(t)?; - Some([angle, v.of(p, near)]) + Some([angle - start, v.of(p, near)]) } } +/// From a revolution's (u, v) to its analytic surface's: the revolution, the angle the +/// surface's u starts at, and how its v is found. +struct Map<'a> { + rev: &'a Revolution, + start: f64, + v: &'a V, +} + /// How the analytic v of a profile point is found. enum V { /// Height along `z` above `foot` (cylinder, cone). @@ -280,7 +305,8 @@ impl V { /// Checks the analytic surface against the revolution over the face's range, 5 × 5 points /// within `tol`, and says whether their natural normals are opposite (the face must be turned /// over), read at the range's middle. -fn agree(record: &b::Surface, domain: [[f64; 2]; 2], analytic: &b::Surface, range: [[f64; 2]; 2], rev: &Revolution, v: &V, tol: f64) -> R { +fn agree(record: (&b::Surface, [[f64; 2]; 2]), analytic: (&b::Surface, [[f64; 2]; 2]), map: Map, tol: f64) -> R { + let ((record, domain), (analytic, range), Map { rev, start, v }) = (record, analytic, map); let bad = Unsupported::Invalid("a revolution its analytic surface does not match"); let s = SurfaceEval::new(record).map_err(|_| bad.clone())?; let a = SurfaceEval::new(analytic).map_err(|_| bad.clone())?; @@ -288,7 +314,7 @@ fn agree(record: &b::Surface, domain: [[f64; 2]; 2], analytic: &b::Surface, rang let near = 0.5 * (range[1][0] + range[1][1]); let normals = |u: f64, w: f64| -> R { let (p, du, dv) = s.derivs(u, w).ok_or(bad.clone())?; - let uv = rev.map(v, [u, w], near).ok_or(bad.clone())?; + let uv = rev.map(v, [u, w], start, near).ok_or(bad.clone())?; let (q, au, av) = a.derivs(uv[0], uv[1]).ok_or(bad.clone())?; if !((p - q).len() <= tol) { return Err(bad.clone()); diff --git a/crates/smlib/src/import/tests.rs b/crates/smlib/src/import/tests.rs index 8f286c3bd9..d4593cf1df 100644 --- a/crates/smlib/src/import/tests.rs +++ b/crates/smlib/src/import/tests.rs @@ -127,9 +127,9 @@ fn a_cube_record_imports_into_smlib() { #[cfg(feature = "native")] #[test] -fn a_skewed_plane_goes_as_an_exact_bilinear_patch() { +fn a_plane_on_other_axes_goes_as_smlibs_plane() { // A tetrahedron: its faces' natural frames are unit and orthogonal, so make one plane's axes - // non-unit to force the NURBS form and check the volume is unchanged. + // non-unit and check it still goes as SMLib's plane, the volume unchanged. let c = [[0., 0., 0.], [6., 0., 0.], [0., 6., 0.], [0., 0., 6.]]; let mut r = polyhedron(&c, &[&[0, 2, 1], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]]); if let Some(Face { surface: Surface::Plane { u_axis, v_axis, .. }, domain, loops }) = r.faces.get_mut(0) { @@ -146,7 +146,7 @@ fn a_skewed_plane_goes_as_an_exact_bilinear_patch() { } r.validate().unwrap(); let a = arrays(&r, true).unwrap(); - assert_eq!(a.ints[&flat::FACE_SURFACE_TYPE][0], i64::from(flat::SURFACE_NURBS)); + assert_eq!(a.ints[&flat::FACE_SURFACE_TYPE][0], i64::from(flat::SURFACE_PLANE)); let b = crate::Brep::from_record(&r, true).unwrap(); assert!((b.volume(1e-7).unwrap() - 36.0).abs() < 1e-6, "{:?}", b.volume(1e-7)); }