forked from 1b-agent/fieldcraft-upstream
mesh: report a kernel failure to partition as the kernel's, not a plane that misses (#283)
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
22f2e0c9cc
commit
d23c2efaec
3 files changed
+56
-10
No files matched your search
@@ -94,7 +94,7 @@ pub const DATUM_DELETE: CommandSpec = CommandSpec {
|
|||||||
pub const PARTITION: CommandSpec = CommandSpec {
|
pub const PARTITION: CommandSpec = CommandSpec {
|
||||||
id: "geometry.partition",
|
id: "geometry.partition",
|
||||||
label: "Partition",
|
label: "Partition",
|
||||||
params: r#"{"name": "Half", "datum": "Mid", "bodies"?: ["Body1"] (default: every body the plane divides)} — cuts bodies of the open design into cells by a datum plane, in memory (the design file is not changed); the model keeps the partition and applies it again whenever the design is opened, after the partitions before it. A cut body `B` becomes the cells `B/a` (on the plane's negative side) and `B/b` (several pieces on a side: `B/a1`, `B/a2`, …). The two faces of the cut are paired by name: `partition:<name>:a` on the cells on the negative side, `partition:<name>:b` on the positive side (`…#0`, `…#1` where a cell has several); every other face keeps the persistent name of the face it is a piece of (`<face>#k` for several pieces on one cell), so sets by face name still select it. Reports the cells made and the cut faces named; a plane that divides no body, or a listed body it does not divide, is an error"#,
|
params: r#"{"name": "Half", "datum": "Mid", "bodies"?: ["Body1"] (default: every body the plane divides)} — cuts bodies of the open design into cells by a datum plane, in memory (the design file is not changed); the model keeps the partition and applies it again whenever the design is opened, after the partitions before it. A cut body `B` becomes the cells `B/a` (on the plane's negative side) and `B/b` (several pieces on a side: `B/a1`, `B/a2`, …). The two faces of the cut are paired by name: `partition:<name>:a` on the cells on the negative side, `partition:<name>:b` on the positive side (`…#0`, `…#1` where a cell has several); every other face keeps the persistent name of the face it is a piece of (`<face>#k` for several pieces on one cell), so sets by face name still select it. Reports the cells made and the cut faces named; a plane that divides no body, a listed body it does not divide, or a body the kernel fails to cut (its message) is an error"#,
|
||||||
form: Some(super::forms::PARTITION),
|
form: Some(super::forms::PARTITION),
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
requires: &[Requirement::Design, Requirement::DatumPlane],
|
requires: &[Requirement::Design, Requirement::DatumPlane],
|
||||||
|
|||||||
@@ -708,6 +708,32 @@ mod tests {
|
|||||||
assert!(r["energy_balance"].as_f64().unwrap() < 0.05, "{r}");
|
assert!(r["energy_balance"].as_f64().unwrap() < 0.05, "{r}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A partition the kernel fails to make is reported as the kernel's failure, never as a plane
|
||||||
|
/// that does not divide the body (#283): x = 148 plainly crosses the crank's half y > 0, whose
|
||||||
|
/// cut fails in the kernel's boolean, and the same plane cuts the half y < 0.
|
||||||
|
#[test]
|
||||||
|
fn a_kernel_failure_to_partition_is_reported_as_such() {
|
||||||
|
use fieldcraft_geometry::Cut;
|
||||||
|
let design = fieldcraft_geometry::open_design(&fieldcraft_geometry::script_design(CRANK_ARM).unwrap()).unwrap();
|
||||||
|
let cut = |name: &str, point: [f64; 3], normal: [f64; 3], bodies: &[&str]| Cut {
|
||||||
|
name: name.into(),
|
||||||
|
point,
|
||||||
|
normal,
|
||||||
|
bodies: bodies.iter().map(|b| b.to_string()).collect(),
|
||||||
|
};
|
||||||
|
let half = cut("Half", [0.0; 3], [0.0, 1.0, 0.0], &[]);
|
||||||
|
let p = design.partitioned(&[half.clone(), cut("X148", [148.0, 0.0, 0.0], [1.0, 0.0, 0.0], &[CRANK_HALF])]).unwrap();
|
||||||
|
assert!(p.errors.is_empty(), "{:?}", p.errors);
|
||||||
|
let p = design.partitioned(&[half, cut("X148", [148.0, 0.0, 0.0], [1.0, 0.0, 0.0], &["CrankArm/b"])]).unwrap();
|
||||||
|
// Should the kernel learn to make this cut, the cut is all there is to check.
|
||||||
|
if p.cuts[1].split.is_empty() {
|
||||||
|
let errors: Vec<&str> = p.errors.iter().filter(|e| e.feature == "partition X148").map(|e| e.message.as_str()).collect();
|
||||||
|
assert!(matches!(errors.as_slice(), [m] if m.starts_with("the kernel could not cut `CrankArm/b`")), "{errors:?}");
|
||||||
|
} else {
|
||||||
|
assert_eq!(p.cuts[1].split, ["CrankArm/b"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `file.sample NAME`, checked clean: ready, no errors or warnings.
|
/// `file.sample NAME`, checked clean: ready, no errors or warnings.
|
||||||
fn open(name: &str) -> Session {
|
fn open(name: &str) -> Session {
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ fn plane_of(cut: &Cut) -> Result<(Plane, [f64; 3])> {
|
|||||||
|
|
||||||
impl Design {
|
impl Design {
|
||||||
/// The design as opened, cut by `cuts` in order (the partitions replace any applied before).
|
/// The design as opened, cut by `cuts` in order (the partitions replace any applied before).
|
||||||
/// A cut that names a body the design does not have, or that does not divide a body it
|
/// A cut that names a body the design does not have, that does not divide a body it names,
|
||||||
/// names, is listed in [`Design::errors`] (feature `partition <name>`), as a feature of the
|
/// or that the kernel fails to make on any body it applies to, is listed in [`Design::errors`] (feature `partition <name>`), as a feature of the
|
||||||
/// design that failed to rebuild; the rest still apply.
|
/// design that failed to rebuild; the rest still apply.
|
||||||
pub fn partitioned(&self, cuts: &[Cut]) -> Result<Design> {
|
pub fn partitioned(&self, cuts: &[Cut]) -> Result<Design> {
|
||||||
if cuts.len() > MAX_CUTS {
|
if cuts.len() > MAX_CUTS {
|
||||||
@@ -116,12 +116,12 @@ impl Design {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match split(&body, plane, *n, cut)? {
|
match split(&body, plane, *n, cut)? {
|
||||||
Some((cells, faces)) => {
|
Split::Cells(cells, faces) => {
|
||||||
report.split.push(body.name.clone());
|
report.split.push(body.name.clone());
|
||||||
report.faces += faces;
|
report.faces += faces;
|
||||||
next.extend(cells);
|
next.extend(cells);
|
||||||
}
|
}
|
||||||
None => {
|
Split::Misses => {
|
||||||
if listed {
|
if listed {
|
||||||
errors.push(FeatureError {
|
errors.push(FeatureError {
|
||||||
feature: feature.clone(),
|
feature: feature.clone(),
|
||||||
@@ -131,6 +131,16 @@ impl Design {
|
|||||||
}
|
}
|
||||||
next.push(body);
|
next.push(body);
|
||||||
}
|
}
|
||||||
|
// Listed or not: the plane may well divide the body, and a cut that silently
|
||||||
|
// leaves it whole would be mistaken for one that does not reach it (#283).
|
||||||
|
Split::Failed(e) => {
|
||||||
|
errors.push(FeatureError {
|
||||||
|
feature: feature.clone(),
|
||||||
|
message: format!("the kernel could not cut `{}` ({e})", body.name),
|
||||||
|
bodies: vec![body.name.clone()],
|
||||||
|
});
|
||||||
|
next.push(body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if next.len() > MAX_CELLS {
|
if next.len() > MAX_CELLS {
|
||||||
return Err(Error::Invalid(format!("the partitions make at most {MAX_CELLS} cells")));
|
return Err(Error::Invalid(format!("the partitions make at most {MAX_CELLS} cells")));
|
||||||
@@ -143,12 +153,22 @@ impl Design {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cut one body: its cells, named, and how many cut faces they have; `None` when the plane does
|
/// What cutting one body gave.
|
||||||
/// not divide it.
|
enum Split {
|
||||||
fn split(body: &Shape, plane: &Plane, n: [f64; 3], cut: &Cut) -> Result<Option<(Vec<Shape>, usize)>> {
|
/// Its cells, named, and how many cut faces they have.
|
||||||
|
Cells(Vec<Shape>, usize),
|
||||||
|
/// The plane does not divide it.
|
||||||
|
Misses,
|
||||||
|
/// The kernel failed to cut it: its message.
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cut one body.
|
||||||
|
fn split(body: &Shape, plane: &Plane, n: [f64; 3], cut: &Cut) -> Result<Split> {
|
||||||
let parts = match guard("partition", || solidcraft_kernel::split_by_plane(&body.body, plane)) {
|
let parts = match guard("partition", || solidcraft_kernel::split_by_plane(&body.body, plane)) {
|
||||||
Ok(p) if p.len() >= 2 => p,
|
Ok(p) if p.len() >= 2 => p,
|
||||||
_ => return Ok(None),
|
Ok(_) => return Ok(Split::Misses),
|
||||||
|
Err(e) => return Ok(Split::Failed(e.to_string())),
|
||||||
};
|
};
|
||||||
let size = body.size()?;
|
let size = body.size()?;
|
||||||
let tol = (MATCH_TOLERANCE * size).max(1e-4);
|
let tol = (MATCH_TOLERANCE * size).max(1e-4);
|
||||||
@@ -180,7 +200,7 @@ fn split(body: &Shape, plane: &Plane, n: [f64; 3], cut: &Cut) -> Result<Option<(
|
|||||||
faces += name_faces(&mut shape, &parent, cut, n, size, tol)?;
|
faces += name_faces(&mut shape, &parent, cut, n, size, tol)?;
|
||||||
out.push(shape);
|
out.push(shape);
|
||||||
}
|
}
|
||||||
Ok(Some((out, faces)))
|
Ok(Split::Cells(out, faces))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A body's faces as matching needs them: each face's analytic surface, name and triangles.
|
/// A body's faces as matching needs them: each face's analytic surface, name and triangles.
|
||||||
|
|||||||
Reference in new issue
Block a user