Blends: an edge is smooth only where its faces are tangent all along it

The edge where a round runs into a cylinder's wall it is tangent to at one end (the bar's front
top round meeting the wall at (40, 0, 8), where the round's contact with the front face meets the
wall's tangent line with it) was refused as smooth, "nothing to round": the faces' normals were
compared at the edge's start alone, which was that tangent end. They are now compared at five
stations from end to end, and an edge is smooth only where its faces meet tangentially at every
one: this edge is a crease everywhere but its end. The wall's seam with the front face, tangent
all along, is still refused as smooth.

The round of that edge with its tangent chain (the top's concave arc) now reaches SMLib, whose
result fails its own check (internal poles, coincident control points, cusps where the round
fades out): its rig case in fillets is listed as known failing for that SMLib defect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 05:50:04 -04:00
committed by Irakli Kourtis
parent 977949819e
commit c9bf97ce49
5 changed files with 230 additions and 8 deletions
+18
View File
@@ -493,6 +493,24 @@ fn rounds_on_a_bar_running_tangent_into_a_cylinder() {
}
}
/// After the peg's front top round, the edge where that round runs into the cylinder's wall: its
/// faces are tangent at its lower end (40, 0, 8), where the round's contact with the front face
/// meets the wall's tangent line with it, and meet at a crease everywhere else. It is no smooth
/// edge (it was refused as one, judged at that end alone); the wall's seam with the front face,
/// tangent all along, still is.
#[test]
fn an_edge_tangent_at_one_end_only_is_not_smooth() {
let mut s = Session::default();
peg(&mut s);
run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 10]], "radius": 2}));
let fillet = |s: &mut Session, at: [f64; 3]| s.execute("solid.fillet", &json!({"edges": [at], "radius": 1, "tangent_chain": false}));
if let Err(e) = fillet(&mut s, [37.2641, 0.4827, 9.3025]) {
assert!(!e.to_string().contains("is smooth"), "{e}");
}
let seam = fillet(&mut s, [40.0, 0.0, 4.0]).unwrap_err().to_string();
assert!(seam.contains("is smooth: its faces meet tangentially"), "{seam}");
}
/// The top outline of two overlapping cylinders whose concave edges were rounded: the walls of
/// the rounds meet the cylinders along the rolling-ball contact lines, straight but not lines.
#[test]
+18 -7
View File
@@ -682,17 +682,28 @@ fn normal_reach(rec: &Faces) -> f64 {
rec.rec.tolerance.max(1e-7) * 10.0
}
/// Whether edge `e`'s two faces meet tangentially (within [`TANGENT_DEGREES`]) at its start: a
/// smooth edge, with no corner to blend.
/// Stations along an edge, as fractions of its interval, where [`smooth_join`] compares its
/// faces' normals: its ends, and between them.
const SMOOTH_STATIONS: [f64; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
/// Whether edge `e`'s two faces meet tangentially (within [`TANGENT_DEGREES`]) all along it: a
/// smooth edge, with no corner to blend. An edge whose faces are tangent at one end only (a
/// round running into a wall it is tangent to where the wall meets its face) is a crease
/// everywhere else, which is not smooth.
fn smooth_join(rec: &Faces, e: usize) -> bool {
let cos = TANGENT_DEGREES.to_radians().cos();
let accept = normal_reach(rec);
let faces: Vec<u32> = rec.faces_of(e).into_iter().collect();
let (Some(&a), Some(&b), Some((p, _))) = (faces.first(), faces.get(1), rec.ends(e)) else { return false };
match (rec.normal_at(a as usize, p, accept), rec.normal_at(b as usize, p, accept)) {
(Some(na), Some(nb)) => faces.len() == 2 && na.dot(nb) > cos,
_ => false,
}
let ([a, b], Some(edge)) = (faces.as_slice(), rec.rec.edges.get(e)) else { return false };
let Ok(curve) = CurveEval::new(edge.parameters()) else { return false };
let [t0, t1] = edge.interval;
SMOOTH_STATIONS.iter().all(|s| {
let Some(p) = curve.point(t0 + (t1 - t0) * s) else { return false };
match (rec.normal_at(*a as usize, p, accept), rec.normal_at(*b as usize, p, accept)) {
(Some(na), Some(nb)) => na.dot(nb) > cos,
_ => false,
}
})
}
/// `seeds` (edges in the record's order, each once), then every other edge that continues one
@@ -0,0 +1,10 @@
{
"sources": [
"tangency-labs/solidcraft#896",
"tangency-labs/solidcraft#1207"
],
"what": "after the round of a bar's top edge running into a cylinder, an r2 round of the edge where that round meets the cylinder's wall (tangent at its lower end only, a crease elsewhere) with its tangent chain (the concave arc of the top): it was refused as a smooth edge",
"source": "invariants",
"method": "a bar 40 x 16 x 10 whose far end is a half disc R8 about (40, 8), a disc R8 about the same centre sketched on its top (the top's outline projected) and extruded 10 up, joined: the bar's sides run tangent into the cylinder, V0 = 6400 + 960 pi; then the front round (the r2 round of the front top edge (y = 0, z = 10) runs into the cylinder's wall: removed = integral over y in [0, 2] of (2 - sqrt(4 - (2 - y)^2)) (40 - sqrt(64 - (8 - y)^2)) dy = 32.3064821881 (Simpson)); the second round fades to nothing at the edge's tangent end, no closed form: one valid body, adding material (concave), so V > V0 - removed = 9383.622465 and below V0",
"body_count": 1
}
@@ -0,0 +1,182 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Peg"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0,
0
],
[
40,
0
]
]
}
},
{
"command": "sketch.arc.center_point",
"params": {
"center": [
40,
8
],
"start": [
40,
0
],
"end": [
40,
16
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
40,
16
],
[
0,
16
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0,
16
],
[
0,
0
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Peg",
"distance": 10,
"body_name": "Peg"
}
},
{
"command": "sketch.create",
"params": {
"plane": {
"origin": [
0,
0,
10
],
"x_dir": [
1,
0,
0
],
"y_dir": [
0,
1,
0
]
},
"name": "Top"
}
},
{
"command": "sketch.project",
"params": {
"refs": [
{
"face": [
2,
8,
10
]
}
]
}
},
{
"command": "sketch.circle.center",
"params": {
"center": [
40,
8
],
"radius": 8
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Top",
"distance": 10,
"operation": "join",
"targets": [
"Peg"
],
"profiles": [
{
"point": [
40,
8
]
}
],
"exact_regions": true
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
20,
0,
10
]
],
"radius": 2
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
37.2641,
0.4827,
9.3025
]
],
"radius": 2
}
}
]
}
+2 -1
View File
@@ -15,13 +15,13 @@
1080-fillet-sloped-basin-1-mm-rim-r4-4-edges not supported yet: a convex round past its face is not yet built by subtraction
1113-chamfer-arc-top-edge-full-wall-height-refused builds the chamfer as tall as the wall where it should be refused as too large
1207-fillet-bar-top-edge-after-boss-arc-round-r0-5 SMLib's check refuses the round: internal coincident control points in a B-spline curve (an internal pole)
1207-fillet-corner-after-round-running-into-cylinder-solver-residues SMLib's check refuses the round: its corner's surface of revolution is close to singular at its boundary (SmSurface test 5)
1668-fillet-outer-top-edge-of-thin-wall-r7-past-inside-corner a slit along the hollow, 0.507 mm^3 off: 38458.389 is V0 less the round's whole section along 104, so the section's part over the hollow (0.005068 mm^2 along 100), which is not material, is taken anyway
190-fillet-disc-rim-past-its-2mm-wall SMLib status 1001: a convex round past its face is not yet built by subtraction
2852-fillet-outer-round-r30-past-inner-round-refused refused for another reason: SMLib's result is not a closed solid, not too large naming 25.24
2893-chamfer-distance-and-angle-past-the-wall-refused refused for another reason: SMLib SM_ERR_INVALID_INPUT, not a check saying the angle runs past the wall
2893-chamfer-two-distances-elliptical-rim SMLib status 1001 on the elliptical rim's chamfer
2893-chamfer-two-distances-past-the-bottom-refused refused for another reason: SMLib status 1001, not too large
1207-fillet-corner-after-round-running-into-cylinder-solver-residues SMLib's check refuses the round: its corner's surface of revolution is close to singular at its boundary (SmSurface test 5)
4782-fillet-bar-edge-wider-than-both-faces SMLib status 1001: a convex round past both its faces is not yet built by subtraction
5158-fillet-round-just-past-a-10mm-face SMLib status 1001: a convex round 0.01 past its face is not yet built by subtraction (10.0001-10.04 open)
5835-chamfer-sloped-basin-1-mm-rim-0-5-4-edges SMLib status 1001 on the chamfers meeting at the rim's re-entrant corners
@@ -33,6 +33,7 @@
851-chamfer-knife-edge-tip-2-box-less-cylinder SMLib SM_ERR_INVALID_INPUT on the knife edge (opposite normals on its two faces)
851-chamfer-knife-edge-tip-2-sketched SMLib SM_ERR_INVALID_INPUT on the knife edge (opposite normals on its two faces)
851-chamfer-knife-edge-tip-5 SMLib SM_ERR_INVALID_INPUT on the knife edge (opposite normals on its two faces)
896-fillet-edge-tangent-at-one-end-into-cylinder SMLib's check refuses the round that fades out at the edge's tangent end: internal poles, coincident control points, cusps
9-fillet-corner-curvature-with-reduce-fit not supported yet: solid.fillet builds corner g2 with the exact fit only
9-fillet-corner-curvature-with-taper-fit not supported yet: solid.fillet builds corner g2 with the exact fit only
926-chamfer-top-edge-ending-on-boss-wall-above-it not supported yet: the chamfer is refused as larger than its faces where it ends on the boss's wall