Tangent hole contacts split no faces, consistently (#307) (#364)

* Tangent hole contacts: every symmetric contact gives the same faces (#307)

Three things made the four symmetric contacts of reference case 42 differ:

- The 2D prism boolean's ray-parity test miscounted a ray grazing the bottom
  of a patterned hole, whose two halves are read back with centres a few ulps
  apart: one half counted the touch, the other missed it. The loop chaining
  then failed and those cuts fell back to the general boolean, which treats
  circles tangent within its tolerance as overlapping and splits the hole.
  The ray is now moved off vertices and arc extremes by far less than any
  modelling tolerance.
- The prism result's volume check used a coarser mesh than the operands, so
  a plate with many holes read too large and correct results were rejected.
- The merged topology did not join a hole's two pieces across the line where
  two tangent holes touch (four faces meet there).

Case 42 now gives 39 faces (33 holes, 6 planes); its expected.json pins it.

Fixes #307

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

* Kernel test: tangent hole pinch no longer splits the wall (#307)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 15:01:03 -04:00
1 parent a5cc1243e3
commit 57e6396fbb
6 files changed
+93 -5

No files matched your search

+17
View File
@@ -186,6 +186,23 @@ fn repeated_source_face_profile_requires_and_uses_the_exact_active_occurrence()
}
}
/// #307: four pairs of holes touching at one line each (a ring pattern against a grid). Every
/// contact is the same up to rounding, so every hole is one face: 33 holes and 6 planes.
#[test]
fn tangent_hole_contacts_split_no_faces() {
let recipe: Value = serde_json::from_str(RECIPE_307).unwrap();
let mut s = Session::default();
for c in recipe["commands"].as_array().unwrap() {
s.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
}
let m = s.execute("inspect.measure", &json!({})).unwrap();
let body = &m["bodies"][0];
assert_eq!(body["face_types"], json!({"cylinder": 33, "plane": 6}), "{m}");
assert_eq!(body["faces"], 39, "{m}");
}
const RECIPE_307: &str = r#"{"commands": [{"command": "sketch.create", "params": {"name": "Plate_sketch", "plane": "XY"}}, {"command": "sketch.line", "params": {"construction": false, "ids": ["l1"], "points": [[-50, -50], [50, -50]]}}, {"command": "sketch.line", "params": {"construction": false, "ids": ["l2"], "points": ["l1.end", [50, 50]]}}, {"command": "sketch.line", "params": {"construction": false, "ids": ["l3"], "points": ["l2.end", [-50, 50]]}}, {"command": "sketch.line", "params": {"construction": false, "ids": ["l4"], "points": ["l3.end", "l1.start"]}}, {"command": "sketch.finish", "params": {}}, {"command": "solid.extrude", "params": {"body_names": ["Plate"], "direction": "positive", "distance": 5, "name": "Plate", "operation": "new", "profiles": [{"loops": [{"curves": ["l1", "l2", "l3", "l4"], "outer": true}], "sketch": "Plate_sketch"}], "sketch": "Plate_sketch"}}, {"command": "sketch.create", "params": {"name": "HoleA", "plane": "XY"}}, {"command": "sketch.circle.center", "params": {"center": [-30, -30], "construction": false, "id": "c1", "radius": 5}}, {"command": "sketch.finish", "params": {}}, {"command": "solid.extrude", "params": {"body_names": ["Plate"], "direction": "positive", "name": "HoleA", "operation": "cut", "profiles": [{"loops": [{"curves": ["c1"], "outer": true}], "sketch": "HoleA"}], "sketch": "HoleA", "through_all": true}}, {"command": "solid.pattern.rectangular", "params": {"count1": 5, "count2": 5, "dir1": [1, 0, 0], "dir2": [0, 1, 0], "features": ["HoleA"], "name": "GridPattern", "spacing1": 15, "spacing2": 15}}, {"command": "sketch.create", "params": {"name": "HoleB", "plane": "XY"}}, {"command": "sketch.circle.center", "params": {"center": [25, 0], "construction": false, "id": "c1", "radius": 5}}, {"command": "sketch.finish", "params": {}}, {"command": "solid.extrude", "params": {"body_names": ["Plate"], "direction": "positive", "name": "HoleB", "operation": "cut", "profiles": [{"loops": [{"curves": ["c1"], "outer": true}], "sketch": "HoleB"}], "sketch": "HoleB", "through_all": true}}, {"command": "solid.pattern.circular", "params": {"angle": 360, "axis": {"dir": [0, 0, 1], "origin": [0, 0, 0]}, "count": 8, "features": ["HoleB"], "name": "RingPattern"}}]}"#;
/// #308: a blind cut ending inside a cylinder leaves thin strips of the cylinder wall beside
/// the cut; they must count as cylinder, not as a sphere or a freeform face.
#[test]