test(kernel): re-measure the two wrong M4-K split fixtures
Both failures were wrong expectations in the WIP fixture, not defects in `evaluate_split_faces`. Neither tolerance was widened; both numbers are what OCCT actually builds, and the reason each moved is recorded at the assertion. **The fillet on a split fragment ran the whole edge, not the fragment.** `BRepFilletAPI_MakeFillet::Add` builds its contour by PROPAGATION along tangent-continuous edges, and a face split leaves the wall/top edge collinear across the imprint, so the band is 40 mm and not the fragment's 10 (17965.66370614359, i.e. 18000 − (1 − π/4)·r²·40). Same at the moved plane, which is why v2's volume is now identical to v1's — the edit is proved by the fragment's centroid moving 5 → 6, which the test already asserted, not by a volume. **The chain cutter's L stopped inside the body.** Measured: a cutter whose imprint ENDS in the middle of a face leaves a dangling edge on it and reading the result's faces back out of the façade throws — and this is not a chain-only limitation, a plane cutter too small to cross does the same. The fixture's L now leaves through the +X wall (four crossed faces, ten faces, eight fragments), and the partial imprint is pinned as a refusal through both cutter kinds instead of being the accident that made the test red. It fails closed, so the operation ships; giving that refusal a name of its own is owed work, and the assertion is what will notice when the error stops being the opaque one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -267,15 +267,27 @@ fn a_cutter_that_misses_the_body_splits_nothing() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A chain cutter, on the two chain shapes the operation is proven on: an
|
||||
/// open two-segment L that crosses clean OUT of the body, and a lone
|
||||
/// circle. Only the faces the imprint fully crosses split.
|
||||
///
|
||||
/// **A cutter that stops INSIDE a face is refused** — measured during the
|
||||
/// M4-K closeout, and it is not a chain-only limitation: an imprint that
|
||||
/// ends in the middle of a face leaves a dangling edge on it, and reading
|
||||
/// the result's faces back out of the façade throws. It fails CLOSED,
|
||||
/// which is why the operation ships; giving that refusal a name of its own
|
||||
/// is owed work, and the assertion below is what will notice when the
|
||||
/// error stops being the opaque one.
|
||||
#[test]
|
||||
fn a_chain_cutter_splits_only_the_faces_it_crosses() {
|
||||
let mut kernel = OcctKernel::new();
|
||||
let mut ids = Counter(0);
|
||||
let mut box_table = NameTable::new();
|
||||
let block = evaluate_box(&mut kernel, &mut box_table, &mut ids, 40.0, 30.0, 15.0).unwrap();
|
||||
// An open L on the sketch plane at z = 7.5: (10,−5)→(10,15)→(30,15),
|
||||
// swept ±20 — it crosses the top and bottom (the L imprints on both)
|
||||
// and exits through the −Y wall.
|
||||
// An open L on the sketch plane at z = 7.5: (10,−5)→(10,15)→(45,15),
|
||||
// swept ±20. It enters through the −Y wall and leaves through the +X
|
||||
// wall, so its imprint crosses four faces: both caps and those two
|
||||
// walls.
|
||||
let cutter = SplitCutter::Chain {
|
||||
segments: vec![
|
||||
SegmentGeometry::Line {
|
||||
@@ -284,7 +296,7 @@ fn a_chain_cutter_splits_only_the_faces_it_crosses() {
|
||||
},
|
||||
SegmentGeometry::Line {
|
||||
start: [10.0, 15.0],
|
||||
end: [30.0, 15.0],
|
||||
end: [45.0, 15.0],
|
||||
},
|
||||
],
|
||||
placement: vernier_occt_sys::Placement {
|
||||
@@ -298,16 +310,31 @@ fn a_chain_cutter_splits_only_the_faces_it_crosses() {
|
||||
let cut = split(&mut kernel, &mut split_table, &mut ids, &block, &cutter).unwrap();
|
||||
assert_eq!(cut.summary.topology.solids, 1);
|
||||
close(cut.summary.geometry.volume, 18000.0, "volume unchanged");
|
||||
assert_eq!(
|
||||
cut.faces.len(),
|
||||
7,
|
||||
"one wall is split in two: the −Y wall the L passes through"
|
||||
);
|
||||
let ymin = role(&box_table, OpRole::BoxYMin);
|
||||
assert!(matches!(cut.verdicts.get(ymin), Some(Verdict::Split(parts)) if parts.len() == 2));
|
||||
assert_eq!(split_table.len(), 2);
|
||||
assert_eq!(cut.faces.len(), 10, "four crossed faces split in two");
|
||||
for crossed in [
|
||||
OpRole::BoxYMin,
|
||||
OpRole::BoxXMax,
|
||||
OpRole::BoxZMin,
|
||||
OpRole::BoxZMax,
|
||||
] {
|
||||
let parent = role(&box_table, crossed);
|
||||
assert!(
|
||||
matches!(cut.verdicts.get(parent), Some(Verdict::Split(parts)) if parts.len() == 2),
|
||||
"{crossed:?} must split, got {:?}",
|
||||
cut.verdicts.get(parent)
|
||||
);
|
||||
}
|
||||
for missed in [OpRole::BoxXMin, OpRole::BoxYMax] {
|
||||
let id = role(&box_table, missed);
|
||||
assert_eq!(
|
||||
cut.verdicts.get(id),
|
||||
Some(&Verdict::Continued(id)),
|
||||
"{missed:?} is not crossed"
|
||||
);
|
||||
}
|
||||
assert_eq!(split_table.len(), 8, "eight fragments, nothing else");
|
||||
|
||||
// A closed chain (a circle alone) and a wrongly-declared one.
|
||||
// A closed chain: a circle alone imprints a disc on both caps.
|
||||
let ring = SplitCutter::Chain {
|
||||
segments: vec![SegmentGeometry::Circle {
|
||||
center: [20.0, 15.0],
|
||||
@@ -317,12 +344,66 @@ fn a_chain_cutter_splits_only_the_faces_it_crosses() {
|
||||
half_extent_mm: 20.0,
|
||||
closed: true,
|
||||
};
|
||||
let ringed = split(&mut kernel, &mut split_table, &mut ids, &block, &ring).unwrap();
|
||||
let mut ring_table = NameTable::new();
|
||||
let ringed = split(&mut kernel, &mut ring_table, &mut ids, &block, &ring).unwrap();
|
||||
assert_eq!(
|
||||
ringed.faces.len(),
|
||||
8,
|
||||
"the circle imprints a disc on the top and the bottom"
|
||||
);
|
||||
close(ringed.summary.geometry.volume, 18000.0, "volume unchanged");
|
||||
|
||||
// A cutter whose imprint stops inside a face: refused, not shipped.
|
||||
let stops_inside = SplitCutter::Chain {
|
||||
segments: vec![SegmentGeometry::Line {
|
||||
start: [10.0, -5.0],
|
||||
end: [10.0, 15.0],
|
||||
}],
|
||||
placement: vernier_occt_sys::Placement {
|
||||
oz: 7.5,
|
||||
..vernier_occt_sys::XY_AT_Z0
|
||||
},
|
||||
half_extent_mm: 20.0,
|
||||
closed: false,
|
||||
};
|
||||
let mut dangling_table = NameTable::new();
|
||||
assert!(
|
||||
matches!(
|
||||
split(
|
||||
&mut kernel,
|
||||
&mut dangling_table,
|
||||
&mut ids,
|
||||
&block,
|
||||
&stops_inside
|
||||
),
|
||||
Err(EvaluateError::Kernel(KernelError::Facade(
|
||||
FacadeError::Occt
|
||||
)))
|
||||
),
|
||||
"an imprint that ends inside a face must fail closed"
|
||||
);
|
||||
// Same limitation through a PLANE cutter too small to cross: it is
|
||||
// the imprint that matters, not the cutter's kind.
|
||||
let mut small_table = NameTable::new();
|
||||
assert!(matches!(
|
||||
split(
|
||||
&mut kernel,
|
||||
&mut small_table,
|
||||
&mut ids,
|
||||
&block,
|
||||
&SplitCutter::Plane {
|
||||
point: [20.0, 0.0, 0.0],
|
||||
normal: [1.0, 0.0, 0.0],
|
||||
half_extent_mm: 5.0,
|
||||
}
|
||||
),
|
||||
Err(EvaluateError::Kernel(KernelError::Facade(
|
||||
FacadeError::Occt
|
||||
)))
|
||||
));
|
||||
|
||||
// The two input refusals: a chain that lies about closing, and a
|
||||
// degenerate plane normal.
|
||||
let lie = SplitCutter::Chain {
|
||||
segments: vec![SegmentGeometry::Line {
|
||||
start: [0.0, 0.0],
|
||||
@@ -391,11 +472,18 @@ fn a_fillet_on_a_split_fragment_survives_moving_the_split_plane() {
|
||||
sibling: 0,
|
||||
})
|
||||
.unwrap();
|
||||
// The band runs the fragment's 10 mm; the removed material is a
|
||||
// quarter-round's complement: (1 − π/4)·r²·L.
|
||||
// RE-MEASURED (M4-K closeout). The fillet does NOT stop at the
|
||||
// fragment: `BRepFilletAPI_MakeFillet::Add` builds its contour by
|
||||
// PROPAGATION along tangent-continuous edges, and a face split leaves
|
||||
// the wall/top edge collinear across the imprint, so the band runs the
|
||||
// block's whole 40 mm. That is the point of the fixture — the
|
||||
// fragment's IDENTITY is what the split gives the fillet, not a
|
||||
// shorter edge — so the number is asserted as what OCCT builds:
|
||||
// 18000 − (1 − π/4)·r²·40. The removed material is a quarter-round's
|
||||
// complement.
|
||||
close(
|
||||
f1.summary.geometry.volume,
|
||||
18000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 10.0,
|
||||
18000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 40.0,
|
||||
"v1 volume",
|
||||
);
|
||||
|
||||
@@ -432,10 +520,13 @@ fn a_fillet_on_a_split_fragment_survives_moving_the_split_plane() {
|
||||
.unwrap();
|
||||
assert!(f2.live.contains(&band), "the fillet face keeps its id");
|
||||
assert_eq!(f1.live, f2.live, "zero churn across the plane edit");
|
||||
// Unchanged for the same reason: the contour propagates past the
|
||||
// moved imprint, so the band is 40 mm at both plane positions. The
|
||||
// edit is proved by the fragment's centroid above, not by a volume.
|
||||
close(
|
||||
f2.summary.geometry.volume,
|
||||
18000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 12.0,
|
||||
"v2 volume: the band is 12 long now",
|
||||
18000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 40.0,
|
||||
"v2 volume: the band still runs the whole edge",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user