forked from 1b-agent/fieldcraft-upstream
geometry: a partition the kernel can only nudge off its plane is refused (#427)
The kernel's last-resort boolean retry moves the tool 1.3 µm and turns it 2 µrad. For a partition that puts the cut beside the face it was meant to run along and leaves a cell bounded by slivers (the open boundary at x = 130 on the crank half). halves() now refuses a result-altering retry and names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
1af87f1f98
commit
3d38753cad
4 files changed
+56
-8
No files matched your search
@@ -945,7 +945,8 @@ pub(crate) mod tests {
|
||||
|
||||
/// #384: cuts stay on their plane (the kernel's own split put them 1.8 µm off it on the air
|
||||
/// of the enclosure, so the cut faces were not even recognized), and a plane along an
|
||||
/// existing face or through a hole's axis is a warning naming the plane, the body and where.
|
||||
/// existing face or through a hole's axis is a warning naming the plane, the body and where,
|
||||
/// or refused where the kernel could only cut it off the plane.
|
||||
#[test]
|
||||
fn cuts_stay_on_the_plane_and_risky_ones_are_named() {
|
||||
let (_dir, mut s) = pi5();
|
||||
@@ -972,11 +973,11 @@ pub(crate) mod tests {
|
||||
assert!(!cut.is_empty() && cut.iter().all(|x| (x - 16.5).abs() < 1e-9), "{cell}: {cut:?}");
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
// Along the side of the heat sink's pedestal: named.
|
||||
let r = plane(&mut s, "Side", 34.0);
|
||||
let warnings = r["warnings"].to_string();
|
||||
assert!(warnings.contains("partition `PSide`: the plane lies on face `F45:-x` of `Air`"), "{r}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
// Along the side of the heat sink's pedestal: the kernel only cuts with its box nudged
|
||||
// off the plane, so the cut is refused, saying why (#427).
|
||||
s.execute("geometry.datum", &json!({"name": "Side", "by": {"type": "offset", "plane": "yz", "offset": 34.0}})).unwrap();
|
||||
let e = s.execute("geometry.partition", &json!({"name": "PSide", "datum": "Side", "bodies": ["Air"]})).unwrap_err().to_string();
|
||||
assert!(e.contains("could not cut `Air`") && e.contains("only made off the plane"), "{e}");
|
||||
// Through a standoff's hole: the cell whose boundary breaks at the hole's edge is named.
|
||||
let r = plane(&mut s, "Axis", 74.5);
|
||||
let warnings = r["warnings"].to_string();
|
||||
|
||||
@@ -769,6 +769,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A cut at x = 130 runs along the pocket's end wall (#427), and the kernel only makes it
|
||||
/// with its cutting box nudged 1.3 µm and 2 µrad off the plane: on the half y > 0 that left a
|
||||
/// cell bounded by slivers next to the wall, which `mesh.cells` refused with an open boundary
|
||||
/// at x = 130. The partition refuses such a cut, saying why, on the whole crank and on the
|
||||
/// half, and leaves the body whole; the plane moved past the pocket (x = 136) cuts the half
|
||||
/// into cells that mesh.
|
||||
#[test]
|
||||
fn a_cut_the_kernel_can_only_nudge_off_its_plane_is_refused() {
|
||||
let datum = |name: &str, x: f64| json!({"name": name, "by": {"type": "offset", "plane": "yz", "offset": x}});
|
||||
let refused = |s: &mut Session, body: &str| {
|
||||
let e = s.execute("geometry.partition", &json!({"name": "X130", "datum": "X130", "bodies": [body]})).unwrap_err().to_string();
|
||||
assert!(e.contains(&format!("could not cut `{body}`")) && e.contains("only made off the plane"), "{e}");
|
||||
};
|
||||
let names = |s: &mut Session| -> Vec<String> {
|
||||
let r = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||
r.as_array().unwrap().iter().filter_map(|b| b["name"].as_str().map(String::from)).collect()
|
||||
};
|
||||
let bytes = fieldcraft_geometry::script_design(CRANK_ARM).unwrap();
|
||||
let (_dir, path) = crate::tests::design_file(&bytes);
|
||||
let mut whole = Session::default();
|
||||
whole.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||
whole.execute("geometry.datum", &datum("X130", 130.0)).unwrap();
|
||||
refused(&mut whole, "CrankArm");
|
||||
assert_eq!(names(&mut whole), ["CrankArm"]);
|
||||
|
||||
let mut s = open("crank-mirror");
|
||||
s.execute("geometry.datum", &datum("X130", 130.0)).unwrap();
|
||||
refused(&mut s, "CrankArm/b");
|
||||
assert_eq!(names(&mut s), ["CrankArm/a", "CrankArm/b"]);
|
||||
s.execute("geometry.datum", &datum("X136", 136.0)).unwrap();
|
||||
let r = s.execute("geometry.partition", &json!({"name": "X136", "datum": "X136", "bodies": ["CrankArm/b"]})).unwrap();
|
||||
assert_eq!(r["cells"], json!(["CrankArm/b/a", "CrankArm/b/b"]), "{r}");
|
||||
s.execute("mesh.cells", &json!({"bodies": ["CrankArm/b/a", "CrankArm/b/b"], "default": "extrude", "size": 3})).unwrap();
|
||||
}
|
||||
|
||||
/// The whole crank extruded all-hex (#198): its back pocket's floor is a rounded rectangle,
|
||||
/// straight sides running tangent into corner arcs, so the region is a disk whose O-grid's
|
||||
/// middle block follows its 6:1 elongation; a square middle block squashed its elements to a
|
||||
|
||||
@@ -187,6 +187,10 @@ impl Design {
|
||||
/// several times the body's size, and its boolean then puts the cut up to a few micrometres off
|
||||
/// the plane (1.8 µm on a 140 mm body), off the faces it should meet (#384); boxes of the body's own
|
||||
/// size keep the cut on the plane.
|
||||
///
|
||||
/// A boolean the kernel could only make by moving one body against the other (its last-resort
|
||||
/// tool nudge, micrometres and microradians) is refused: the cut would lie off the plane, next to
|
||||
/// the face it was meant to run along, and leave a cell bounded by slivers no mesher takes (#427).
|
||||
fn halves(body: &Shape, plane: &Plane) -> solidcraft_kernel::Result<Vec<solidcraft_kernel::Body>> {
|
||||
let bbox = solidcraft_kernel::measure(&body.body)?.bbox;
|
||||
let reach = bbox.diagonal().max(LINEAR_REACH);
|
||||
@@ -203,7 +207,14 @@ fn halves(body: &Shape, plane: &Plane) -> solidcraft_kernel::Result<Vec<solidcra
|
||||
let mut out = Vec::with_capacity(2);
|
||||
for (lo, hi) in [(0.0, reach), (-reach, 0.0)] {
|
||||
let Some(side) = solidcraft_kernel::extrude(plane, std::slice::from_ref(&square), lo, hi)?.pop() else { continue };
|
||||
out.extend(solidcraft_kernel::boolean(&body.body, &side, solidcraft_kernel::BoolOp::Intersect)?);
|
||||
let (cut, report) =
|
||||
solidcraft_kernel::capture_boolean_diagnostics(|| solidcraft_kernel::boolean(&body.body, &side, solidcraft_kernel::BoolOp::Intersect));
|
||||
if let Some(moved) = report.warning() {
|
||||
return Err(solidcraft_kernel::KernelError::Failed(format!(
|
||||
"the cut is only made off the plane ({moved}); the plane runs along a face or edge of the body: move it off (by a fraction of the element size)"
|
||||
)));
|
||||
}
|
||||
out.extend(cut?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ Before the first mesh command:
|
||||
- **Curved splits are allowed.** A cut does not have to be a plane: use a cylindrical or offset
|
||||
surface (a ring around a boss, an offset of a fillet) when a plane would create slivers.
|
||||
- **Never cut along an existing face** (a plane coincident with a face within tolerance leaves
|
||||
micrometre slivers, #384) and never through the axis of a small hole (degenerate tessellation at the
|
||||
micrometre slivers, #384; where the kernel can only make it by nudging its cut off the plane,
|
||||
`geometry.partition` refuses it, #427) and never through the axis of a small hole (degenerate tessellation at the
|
||||
hole edge). Offset the cut to just outside the feature instead.
|
||||
- **Keep cuts consistent across touching bodies** so their meshes can match, or tie them
|
||||
deliberately (`interaction.tie`; linear faces only today).
|
||||
|
||||
Reference in new issue
Block a user