From 4a4911b47319e97eca785ad3e6be5bc29f41f31d Mon Sep 17 00:00:00 2001 From: DuBearte <130190089+DuBearte@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:26:32 -0400 Subject: [PATCH] Mesh export: twisted ruled faces are divided to the chord tolerance (#1666) (#1670) Thanks @DuBearte. --- crates/engine/src/tests.rs | 37 +++++++++++++++++++ vendor/README.md | 5 +++ .../src/tessellation/triangulation.rs | 9 ++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 23e33e46..d5d11263 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1196,6 +1196,43 @@ fn extrude_auto_operation() { assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0 - 100.0 * 8.0) < 1e-6); } +/// #1666: a loft between two 18-point polylines has twisted (bilinear) side faces. Each was +/// meshed as two flat triangles, which cross the surface; the mesh volume was 16 800.6 mm³ +/// against the B-rep's 17 884. Divided to the chord tolerance, the mesh matches the B-rep. +#[test] +fn twisted_ruled_faces_are_meshed_to_the_chord_tolerance() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Bottom"})); + run( + &mut s, + "sketch.line", + json!({"closed": true, "infer": false, "connect": false, "points": [[-26,3],[-24.76384,3],[-16.0568,14.97217],[-8,4.90117],[0,14.90117],[8,4.90117],[16.0568,14.97217],[24.76384,3],[26,3],[26,0],[23.23616,0],[15.9432,10.02783],[8,0.09883],[0,10.09883],[-8,0.09883],[-15.9432,10.02783],[-23.23616,0],[-26,0]]}), + ); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 90.5, "name": "Top"})); + run( + &mut s, + "sketch.line", + json!({"closed": true, "infer": false, "connect": false, "points": [[-26,3],[-24,3],[-16,3],[-8,3],[0,3],[8,3],[16,3],[24,3],[26,3],[26,0],[24,0],[16,0],[8,0],[0,0],[-8,0],[-16,0],[-24,0],[-26,0]]}), + ); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.loft", json!({"sections": [{"sketch": "Bottom"}, {"sketch": "Top"}], "operation": "new"})); + let st = s.model.state(); + let body = &st.bodies.first().unwrap().body; + let exact = solidcraft_kernel::measure(body).unwrap().volume; + let mesh = body.tessellate(0.05).unwrap(); + let volume: f64 = mesh + .triangles + .iter() + .map(|t| { + let p = |i: u32| mesh.positions[i as usize]; + p(t[0]).dot(p(t[1]).cross(p(t[2]))) / 6.0 + }) + .sum(); + assert!(mesh.triangles.len() > 72, "the twisted faces are divided: {} triangles", mesh.triangles.len()); + assert!(rel(volume.abs(), exact) < 2e-3, "mesh {volume} against B-rep {exact}"); +} + /// #1633: to an object, the extrude goes to it on whichever side it is, whatever its /// `direction`: through the body to its far face is a Cut, out to something above is a Join. #[test] diff --git a/vendor/README.md b/vendor/README.md index f10cc609..50842da7 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -155,6 +155,11 @@ Changes: compares the two. - `lib.rs`: compiler warnings no longer fail the build. Example, test and bench targets are dropped (their sources are not vendored). +- `tessellation/triangulation.rs`: adaptive division also splits a cell that bows off its + corners' plane by more than the chord tolerance (a quarter of its twist `p00 - p01 - p10 + + p11`, across that plane, at the middle). A bilinear patch equals the blend of its corners the + division compared against, so a twisted ruled face (a loft between polylines) went out as two + triangles per face (#1666); a flat cell such as a cone's trapezoid between rulings is not split. ## matext4cgmath 0.1.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/matext4cgmath) diff --git a/vendor/truck-meshalgo/src/tessellation/triangulation.rs b/vendor/truck-meshalgo/src/tessellation/triangulation.rs index b1b9b5a6..20ed2e97 100644 --- a/vendor/truck-meshalgo/src/tessellation/triangulation.rs +++ b/vendor/truck-meshalgo/src/tessellation/triangulation.rs @@ -971,7 +971,14 @@ fn bounded_surface_division(surface: &S, (ur, vr): ((f64, let pt10 = surface.subs(u[1], v[0]).to_vec(); let pt11 = surface.subs(u[1], v[1]).to_vec(); let pt = Point3::from_vec(pt00 * (1.0 - p) * (1.0 - q) + pt01 * (1.0 - p) * q + pt10 * p * (1.0 - q) + pt11 * p * q); - let far = p0.distance2(pt) > tol * tol; + // SolidCraft (#1666): a cell is drawn as two triangles, not as the bilinear blend + // of its corners. A twisted cell (a bilinear patch is the blend exactly, so `pt` + // never tells) bows off its corners' plane by a quarter of its twist at the middle, + // across that plane: a cone's trapezoid between two rulings is flat and needs none. + let normal = (pt10 - pt00).cross(pt01 - pt00); + let bow = (pt00 - pt01 - pt10 + pt11).dot(normal) * 0.25; + let twisted = normal.magnitude2() > 0.0 && bow * bow > tol * tol * normal.magnitude2(); + let far = p0.distance2(pt) > tol * tol || twisted; // Which direction bends: refine only that one (a cylinder needs no division // along its rulings). let (su, sv) = ([surface.subs(u[0], v0), surface.subs(u[1], v0)], [surface.subs(u0, v[0]), surface.subs(u0, v[1])]);