Surfaces rig: the cylinder and torus windows the general fill patches, as cases

A 240 deg window through a cylinder's wall and a saddle window in a torus's inner
wall, their cut faces deleted, patched G0 and G1 and stitched, become surfaces rig
cases: the G1 cylinder seals to the whole cylinder's volume within 0.5 % with G1
seams on the window's edges, the G1 torus reaches G1 on its rim, the G0 fills are
one valid solid. They fail today as the unit tests did (the general fill refuses:
it folds) and are on KNOWN_FAILING; the unit tests are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 06:56:46 -04:00
committed by Irakli Kourtis
parent 06f7fa501a
commit 10fe62a2b0
10 changed files with 551 additions and 82 deletions
+1 -82
View File
@@ -9,7 +9,7 @@ use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
use super::{PATCH_ANGLE_TOL_DEG, PatchReport, PatchSpec, face_tolerance, patch_edges_report};
use crate::body::Body;
use crate::bridge::Continuity;
use crate::{BoolOp, boolean, box_solid, copy_faces, cylinder, extrude, measure, patch_edges, stitch, torus};
use crate::{BoolOp, boolean, box_solid, copy_faces, cylinder, extrude, measure, patch_edges, stitch};
/// A body's faces as surfaces over their parameter boxes, with the (u, v) polygon of each face's
/// loops (its exact pcurves, sampled).
@@ -301,87 +301,6 @@ fn an_opened_tube_end_capped_g1_and_g2_meets_the_wall() {
assert!(kappas[1] < kappas[0], "g2 holds the wall's curvature closer than g1");
}
/// A cylinder about z (radius 10, 40 high) with a window `half` either side of the x axis
/// through its wall, from z = 15 to 25; the wall and both ends without the faces the cut made (the
/// window's edges bound only the wall), and a point on each of the window's edges.
fn windowed(half: f64) -> (Body, Body, Vec<Vec3>) {
let solid = cylinder(Vec3::ZERO, Vec3::Z, 10.0, 40.0).unwrap();
let x = 10.0 * half.cos();
let tool = box_solid(Vec3::new(x, -11.0, 15.0), Vec3::new(11.0, 11.0, 25.0)).unwrap();
let cut = boolean(&solid, &tool, BoolOp::Cut).unwrap().unwrap();
let open = curved_and_ends(&cut, &[0.0, 40.0]);
let rim = free_edge_points(&open);
(solid, open, rim)
}
#[test]
fn a_window_cut_through_a_cylinder_wall_past_half_way_fills_g1_close_to_the_cylinder() {
// 240° round: seen along any direction the window folds over itself, so it bounds no height
// field; the old fill refused it.
let half = 2.0 * std::f64::consts::PI / 3.0;
let (solid, open, rim) = windowed(half);
for g in [Continuity::G0, Continuity::G1] {
let name = format!("window {}", g.as_str());
let (fill, r) = patch_edges_report(&open, &rim, &spec(g)).unwrap();
report(&name, &r);
let area = check_face(&name, &open, &fill);
let (gap, angle, _) = along_the_loop(&fill, &radial, &|_| 0.0);
eprintln!("{name}: along the window (1600 points): within {gap:e}, normal within {angle:.5}° of the wall's");
// How far the fill strays from the cylinder it replaces, inside.
let mut stray = 0.0f64;
for (s, [[u0, u1], [v0, v1]], _) in &Faces::of(&fill).faces {
for i in 0..=40 {
for j in 0..=40 {
let p = s.point([u0 + (u1 - u0) * i as f64 / 40.0, v0 + (v1 - v0) * j as f64 / 40.0]).unwrap();
let ang = p.y.atan2(p.x);
if ang.abs() < half - 0.05 && p.z > 15.5 && p.z < 24.5 {
stray = stray.max((Vec2::new(p.x, p.y).len() - 10.0).abs());
}
}
}
}
let exact = 10.0 * 2.0 * half * 10.0;
let closed = stitch(&[&open, &fill], 0.0).unwrap();
let (got, want) = (measure(&closed).unwrap().volume, measure(&solid).unwrap().volume);
eprintln!(
"{name}: area {area:.3} (the cylinder's window {exact:.3}), the fill within {stray:.4} of the cylinder inside, sealed volume {got:.2} against the whole cylinder's {want:.2}"
);
assert!(!closed.is_surface());
if g == Continuity::G1 {
assert!(angle < 0.1, "{name}: the normal turns {angle}° from the wall's");
assert!((area - exact).abs() < 0.01 * exact && stray < 0.25, "{name}: {area} vs {exact}, stray {stray}");
assert!((got - want).abs() < 0.005 * want, "{name}: {got} vs {want}");
}
}
}
#[test]
fn a_saddle_window_in_a_torus_fills_g0_and_g1() {
// The inner wall of a torus (major 20, minor 8) is saddle-shaped; a window 240° round it
// between z = ±3 bounds no height field.
let solid = torus(Vec3::ZERO, 20.0, 8.0).unwrap();
let tool = box_solid(Vec3::new(-6.0, -13.0, -3.0), Vec3::new(13.0, 13.0, 3.0)).unwrap();
let cut = boolean(&solid, &tool, BoolOp::Cut).unwrap().unwrap();
let open = curved_and_ends(&cut, &[]);
let rim = free_edge_points(&open);
// The torus's normal: from the tube's centre circle.
let normal = |p: Vec3| {
let c = Vec3::new(p.x, p.y, 0.0).normalized().unwrap() * 20.0;
(p - c).normalized().unwrap()
};
for g in [Continuity::G0, Continuity::G1] {
let name = format!("torus window {}", g.as_str());
let (fill, r) = patch_edges_report(&open, &rim, &spec(g)).unwrap();
report(&name, &r);
check_face(&name, &open, &fill);
let (gap, angle, _) = along_the_loop(&fill, &normal, &|_| 0.0);
eprintln!("{name}: along the window (1600 points): within {gap:e}, normal within {angle:.5}° of the torus's");
if g == Continuity::G1 {
assert!(angle < 0.1, "{name}: {angle}°");
}
}
}
/// A cylinder about z (radius 10, 40 high) with a pentagonal pocket cut into its wall from the
/// +x side (the pentagon in the yz plane about z = 20, circumradius 6): the wall and both ends
/// copied out, and a point on each of the pocket rim's five edges.
@@ -3,3 +3,7 @@
# that fails and is not listed fails it, and so does a listed case that passes: a fix removes its
# line in the same pull request.
# Baseline: rig-cases-surfaces 45f69c0e5 with SMLib 60b1c6a, aarch64 Linux.
cov-patch-g0-fills-a-240-deg-window-in-a-cylinder not supported yet: the general fill refuses the window (it folds)
cov-patch-g0-fills-a-saddle-window-in-a-torus not supported yet: the general fill refuses the window (it folds)
cov-patch-g1-fills-a-240-deg-window-in-a-cylinder not supported yet: the general fill refuses the window (it folds)
cov-patch-g1-fills-a-saddle-window-in-a-torus not supported yet: the general fill refuses the window (it folds)
@@ -0,0 +1,9 @@
{
"sources": [
"tangency-labs/solidcraft#759"
],
"what": "a window 240 deg round a cylinder's wall (r10 x 40, z 15..25), the faces the cut made taken off and the window patched g0 and stitched",
"source": "invariants",
"method": "the window (as the g1 case) filled G0: the fill meets the window's edges; its shape is the fill's own, so one closed valid solid is the right answer",
"body_count": 1
}
@@ -0,0 +1,99 @@
{
"commands": [
{
"command": "solid.cylinder",
"params": {
"radius": 10,
"height": 40,
"body_name": "Cyl"
}
},
{
"command": "solid.box",
"params": {
"length": 16,
"width": 22,
"height": 10,
"corner": [
-5,
-11,
15
],
"operation": "cut"
}
},
{
"command": "face.delete",
"params": {
"body": "Cyl",
"faces": [
[
-5,
0,
20
],
[
3,
0,
15
],
[
3,
0,
25
]
],
"heal": false
}
},
{
"command": "surface.patch",
"params": {
"body": "Cyl",
"edges": [
[
8.660254037844387,
-5,
15
],
[
0,
10,
15
],
[
8.660254037844387,
-5,
25
],
[
0,
10,
25
],
[
-5,
-8.660254037844387,
20
],
[
-5,
8.660254037844387,
20
]
],
"continuity": "g0",
"body_name": "Lid"
}
},
{
"command": "surface.stitch",
"params": {
"bodies": [
"Cyl",
"Lid"
]
}
}
]
}
@@ -0,0 +1,9 @@
{
"sources": [
"tangency-labs/solidcraft#759"
],
"what": "a window 240 deg round the saddle-shaped inner wall of a torus (R20, r8) between z = -3 and 3, its cut faces taken off, patched g0 and stitched",
"source": "invariants",
"method": "the box x -6..13, y -13..13, z -3..3 opens the inner wall where 12 cos a >= -6 (|a| <= 120 deg): its rim is two arcs on z = +-3 each way and two curves on x = -6, and it bounds no height field. G0: the fill meets the rim. The fill's shape is its own: one closed valid solid is the right answer.",
"body_count": 1
}
@@ -0,0 +1,119 @@
{
"commands": [
{
"command": "solid.torus",
"params": {
"major": 20,
"minor": 8,
"body_name": "Ring"
}
},
{
"command": "solid.box",
"params": {
"length": 19,
"width": 26,
"height": 6,
"corner": [
-6,
-13,
-3
],
"operation": "cut"
}
},
{
"command": "face.delete",
"params": {
"body": "Ring",
"faces": [
[
12.8,
0,
3
],
[
12.8,
0,
-3
],
[
13,
0,
0
],
[
0,
13,
0
],
[
0,
-13,
0
],
[
-6,
12,
0
],
[
-6,
-12,
0
]
],
"heal": false
}
},
{
"command": "surface.patch",
"params": {
"body": "Ring",
"edges": [
[
6.417783810682518,
-10.789476030783234,
-3
],
[
6.417783810682518,
10.789476030783234,
-3
],
[
6.417783810682518,
10.789476030783234,
3
],
[
6.417783810682518,
-10.789476030783234,
3
],
[
-6,
10.39230584418982,
0
],
[
-6,
-10.39230584418982,
0
]
],
"continuity": "g0",
"body_name": "Lid"
}
},
{
"command": "surface.stitch",
"params": {
"bodies": [
"Ring",
"Lid"
]
}
}
]
}
@@ -0,0 +1,48 @@
{
"sources": [
"tangency-labs/solidcraft#759"
],
"what": "a window 240 deg round a cylinder's wall (r10 x 40, z 15..25), the faces the cut made taken off and the window patched g1 and stitched: tangent to the wall it replaces, close to the cylinder",
"source": "analytic",
"method": "the box x -5..11, y -11..11, z 15..25 cuts the wall where 10 cos a >= -5, |a| <= 120 deg: seen from any direction the window folds over itself, so it bounds no height field. G1 to the wall all round, the fill stays close to the cylinder it replaces: the sealed part is the whole cylinder, pi 100 40 = 12566.370614, within 0.5 % (the fill's own shape is not the cylinder's, so that is a bound, not an identity)",
"body_count": 1,
"volume_mm3": 12566.370614359172,
"tolerance": {
"rel": 0.005
},
"continuity": {
"order": "g1",
"edges": [
[
8.660254037844387,
-5,
15
],
[
0,
10,
15
],
[
8.660254037844387,
-5,
25
],
[
0,
10,
25
],
[
-5,
-8.660254037844387,
20
],
[
-5,
8.660254037844387,
20
]
]
}
}
@@ -0,0 +1,99 @@
{
"commands": [
{
"command": "solid.cylinder",
"params": {
"radius": 10,
"height": 40,
"body_name": "Cyl"
}
},
{
"command": "solid.box",
"params": {
"length": 16,
"width": 22,
"height": 10,
"corner": [
-5,
-11,
15
],
"operation": "cut"
}
},
{
"command": "face.delete",
"params": {
"body": "Cyl",
"faces": [
[
-5,
0,
20
],
[
3,
0,
15
],
[
3,
0,
25
]
],
"heal": false
}
},
{
"command": "surface.patch",
"params": {
"body": "Cyl",
"edges": [
[
8.660254037844387,
-5,
15
],
[
0,
10,
15
],
[
8.660254037844387,
-5,
25
],
[
0,
10,
25
],
[
-5,
-8.660254037844387,
20
],
[
-5,
8.660254037844387,
20
]
],
"continuity": "g1",
"body_name": "Lid"
}
},
{
"command": "surface.stitch",
"params": {
"bodies": [
"Cyl",
"Lid"
]
}
}
]
}
@@ -0,0 +1,44 @@
{
"sources": [
"tangency-labs/solidcraft#759"
],
"what": "a window 240 deg round the saddle-shaped inner wall of a torus (R20, r8) between z = -3 and 3, its cut faces taken off, patched g1 and stitched",
"source": "invariants",
"method": "the box x -6..13, y -13..13, z -3..3 opens the inner wall where 12 cos a >= -6 (|a| <= 120 deg): its rim is two arcs on z = +-3 each way and two curves on x = -6, and it bounds no height field. G1: the fill's tangent plane is the torus's all round the rim. The fill's shape is its own: one closed valid solid is the right answer.",
"body_count": 1,
"continuity": {
"order": "g1",
"edges": [
[
6.417783810682518,
-10.789476030783234,
-3
],
[
6.417783810682518,
10.789476030783234,
-3
],
[
6.417783810682518,
10.789476030783234,
3
],
[
6.417783810682518,
-10.789476030783234,
3
],
[
-6,
10.39230584418982,
0
],
[
-6,
-10.39230584418982,
0
]
]
}
}
@@ -0,0 +1,119 @@
{
"commands": [
{
"command": "solid.torus",
"params": {
"major": 20,
"minor": 8,
"body_name": "Ring"
}
},
{
"command": "solid.box",
"params": {
"length": 19,
"width": 26,
"height": 6,
"corner": [
-6,
-13,
-3
],
"operation": "cut"
}
},
{
"command": "face.delete",
"params": {
"body": "Ring",
"faces": [
[
12.8,
0,
3
],
[
12.8,
0,
-3
],
[
13,
0,
0
],
[
0,
13,
0
],
[
0,
-13,
0
],
[
-6,
12,
0
],
[
-6,
-12,
0
]
],
"heal": false
}
},
{
"command": "surface.patch",
"params": {
"body": "Ring",
"edges": [
[
6.417783810682518,
-10.789476030783234,
-3
],
[
6.417783810682518,
10.789476030783234,
-3
],
[
6.417783810682518,
10.789476030783234,
3
],
[
6.417783810682518,
-10.789476030783234,
3
],
[
-6,
10.39230584418982,
0
],
[
-6,
-10.39230584418982,
0
]
],
"continuity": "g1",
"body_name": "Lid"
}
},
{
"command": "surface.stitch",
"params": {
"bodies": [
"Ring",
"Lid"
]
}
}
]
}