engine: feature replies carry no boolean diagnostics

doc's FeatureResult has no boolean_diagnostics any more (truck's Boolean
retry reports, gone with truck; SMLib's boolean has no retries). The
timeline.inspect and doc.inspect replies drop the key.

Tests of the removed reports: tests/boolean_diagnostics.rs is deleted (it
checked truck's retry events and their survival through the cache, undo and
reopen); the assertions that a feature's report had no events go from
features_more_tests, patterns_tests, hole_face_tests and tests/rim_cut (the
rest of each test stays). The CLI corpus's expected replies
(move-bodies, issue-489-stacked-join, issue-616-lid-ribs) drop the key; the
issue-616 one listed a planar_fallback event. docs/robustness.md: its
section on retries becomes one on SMLib's booleans, which have none.

Left to the blends work: engine fuzz.rs (its retry-event counts) and
clearance/interference (capture_boolean_diagnostics, coaxial_overlap,
with_attempt_budget).

Not built: base in progress (kernel-smlib/no-truck 66ab6b7f does not build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:36:40 -04:00
parent 88b1ba5075
commit 1ecf00b5ac
12 changed files with 9 additions and 241 deletions
@@ -90,10 +90,6 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.131943,
@@ -104,10 +100,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 1.0812709999999999,
@@ -118,10 +110,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.450002,
@@ -132,10 +120,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 5.191514000000001,
@@ -54,10 +54,6 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 1.178957,
@@ -68,10 +64,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 83.24342399999999,
@@ -82,10 +74,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.150889,
@@ -96,10 +84,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 94.465481,
@@ -110,10 +94,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 50.426736,
@@ -124,10 +104,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 40.383005,
@@ -138,10 +114,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 7,
"ms": 59.041461000000005,
@@ -152,18 +124,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [
{
"face_shift_mm": 0.0,
"rotation_rad": 0.0,
"strategy": "planar_fallback",
"tolerance_mm": 0.0,
"translation_mm": 0.0
}
],
"truncated": false
},
"error": null,
"id": 8,
"ms": 562.5477970000001,
@@ -174,10 +134,6 @@
"warning": "Boolean retry: exact planar fallback (no displacement)"
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 9,
"ms": 89.56076,
@@ -188,10 +144,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 10,
"ms": 65.330634,
@@ -202,10 +154,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 11,
"ms": 97.229403,
@@ -99,10 +99,6 @@
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.204585,
@@ -113,10 +109,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 2.318662,
@@ -127,10 +119,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.109196,
@@ -141,10 +129,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 0.338859,
@@ -155,10 +139,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 0.312259,
@@ -169,10 +149,6 @@
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 0.23144699999999999,
-1
View File
@@ -1031,7 +1031,6 @@ fn inspect_feature(s: &mut Session, p: &Value) -> Result<Value> {
"id": f.id, "name": f.name, "type": f.kind.type_name(), "suppressed": f.suppressed, "rolled_back": rolled_back,
"definition": def, "omitted": omitted, "inputs": inputs,
"error": r.and_then(|r| r.error.clone()), "warning": r.and_then(|r| r.warning.clone()),
"boolean_diagnostics": r.map(|r| &r.boolean_diagnostics),
}))
}
+5 -5
View File
@@ -1870,7 +1870,7 @@ fn a_ring_cut_round_a_hole_takes_in_the_hole_it_is_drawn_round() {
.ok()
.and_then(|v| v["feature"].as_u64())
.and_then(|id| s.model.result(id))
.is_some_and(|f| f.boolean_diagnostics.events.is_empty());
.is_some_and(|f| f.error.is_none());
(r.is_ok(), before - volume(&mut s), clean)
};
let taken = std::f64::consts::PI * (36.0 - 9.0) * 3.0;
@@ -1932,7 +1932,7 @@ fn a_line_of_text_is_cut_in_one_exact_boolean() {
run(&mut s, "sketch.finish", json!({}));
let id = run(&mut s, "solid.extrude", json!({"distance":-depth,"operation":"cut"}))["feature"].as_u64().unwrap();
let r = s.model.result(id).unwrap();
assert!(r.error.is_none() && r.boolean_diagnostics.events.is_empty(), "{at:?} {depth}: {r:?}");
assert!(r.error.is_none(), "{at:?} {depth}: {r:?}");
before - volume(&mut s)
};
// Straight walls: the material taken grows with the depth, the letters' area each mm.
@@ -1961,7 +1961,7 @@ fn a_line_of_text_cut_through_or_joined_in_relief_is_one_boolean() {
run(&mut s, "sketch.finish", json!({}));
let id = run(&mut s, "solid.extrude", json!({"distance":distance,"operation":op}))["feature"].as_u64().unwrap();
let r = s.model.result(id).unwrap();
assert!(r.error.is_none() && r.boolean_diagnostics.events.is_empty(), "{op}: {r:?}");
assert!(r.error.is_none(), "{op}: {r:?}");
(before, volume(&mut s), s.model.state().bodies.len())
};
let (before, through, pieces) = text("cut", -40.0);
@@ -1995,7 +1995,7 @@ fn a_line_of_text_cut_again_deeper_is_one_exact_boolean() {
run(s, "sketch.finish", json!({}));
let id = run(s, "solid.extrude", json!({"distance":-depth,"operation":"cut"}))["feature"].as_u64().unwrap();
let r = s.model.result(id).unwrap();
assert!(r.error.is_none() && r.boolean_diagnostics.events.is_empty(), "{depth}: {r:?}");
assert!(r.error.is_none(), "{depth}: {r:?}");
before - volume(s)
};
let first = cut(&mut s, 1.0);
@@ -2021,7 +2021,7 @@ fn text_cut_through_a_large_plate_is_one_exact_boolean() {
run(&mut s, "sketch.finish", json!({}));
let id = run(&mut s, "solid.extrude", json!({"distance":-2,"operation":"cut"}))["feature"].as_u64().unwrap();
let r = s.model.result(id).unwrap();
assert!(r.error.is_none() && r.boolean_diagnostics.events.is_empty(), "{w} x {h}: {r:?}");
assert!(r.error.is_none(), "{w} x {h}: {r:?}");
before - volume(&mut s)
};
let (small, large) = (taken(200.0, 120.0, [60.0, 40.0]), taken(2000.0, 1200.0, [860.0, 125.0]));
-1
View File
@@ -326,7 +326,6 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
json!({
"id": f.id, "name": f.name, "type": f.kind.type_name(), "suppressed": f.suppressed, "rolled_back": i >= marker,
"error": r.and_then(|r| r.error.clone()), "warning": r.and_then(|r| r.warning.clone()), "ms": r.map(|r| r.ms),
"boolean_diagnostics": r.map(|r| &r.boolean_diagnostics),
})
})
.collect();
-2
View File
@@ -632,7 +632,6 @@ fn pattern_of_touching_cuts_needs_no_fallback() {
let (mut s, id) = sawtooth_bar(40, true);
let result = s.model.result(id.unwrap()).unwrap();
assert!(result.error.is_none() && result.warning.is_none(), "{result:?}");
assert!(result.boolean_diagnostics.events.is_empty(), "{:?}", result.boolean_diagnostics);
let m = run(&mut s, "inspect.measure", json!({}));
let (mut one, _) = sawtooth_bar(40, false);
let single = run(&mut one, "inspect.measure", json!({}));
@@ -670,7 +669,6 @@ fn pattern_of_cuts_beside_a_round_needs_no_retries() {
let id = created["feature"].as_u64().unwrap();
let result = s.model.result(id).unwrap();
assert!(result.error.is_none() && result.warning.is_none(), "{result:?}");
assert!(result.boolean_diagnostics.events.is_empty(), "{:?}", result.boolean_diagnostics);
}
// The bar, less the rounds' corners and 40 teeth (a 4 x 2 triangle below the top, 16 wide).
let expected = 16000.0 - 400.0 * (1.0 - PI / 4.0) - 40.0 * 4.0 * 16.0;
-1
View File
@@ -343,7 +343,6 @@ fn drilled_sketch_points_break_through_a_sloping_back_face() {
let id = s.doc.find_feature("H").unwrap().id;
let result = s.model.result(id).unwrap();
assert!(result.error.is_none());
assert!(result.boolean_diagnostics.unverified().is_none());
// The cylindrical bore and cone, less the oblique conical cap outside x = z/4.
let r = 2.5;
let k = 59f64.to_radians().tan();
-1
View File
@@ -324,7 +324,6 @@ fn join_of_a_lobed_disc_pattern_is_not_refused() {
}
mod blend_fit;
mod boolean_diagnostics;
mod component_frames;
mod curved_seam_round;
mod flush_join_loft;
@@ -1,84 +0,0 @@
use super::*;
use solidcraft_kernel::BooleanStrategy;
#[test]
fn boolean_retry_warning_and_details_survive_cache_undo_and_reopen() {
let slope = 1f64.to_radians().tan();
let mut session = Session::default();
run(&mut session, "sketch.create", json!({"plane":"XZ","name":"Floor section"}));
run(&mut session, "sketch.line", json!({"points":[[0,0],[100,0],[100,-10.-slope*100.],[0,-10.]],"closed":true}));
run(&mut session, "sketch.finish", json!({}));
run(&mut session, "solid.extrude", json!({"sketch":"Floor section","distance":80,"body_name":"Floor"}));
run(&mut session, "solid.cylinder", json!({"base":[50,40,-5],"radius":10,"height":30,"operation":"cut"}));
run(&mut session, "sketch.create", json!({"plane":"XY","name":"Ring section"}));
run(&mut session, "sketch.circle.center", json!({"center":[50,40],"radius":20}));
run(&mut session, "sketch.circle.center", json!({"center":[50,40],"radius":10}));
run(&mut session, "sketch.finish", json!({}));
run(
&mut session,
"solid.extrude",
json!({"sketch":"Ring section","profiles":[{"point":[65,40]}],"start_offset":-5,"distance":7,"body_name":"Ring"}),
);
let id = run(&mut session, "solid.combine", json!({"target":"Floor","tools":["Ring"],"operation":"join"}))["feature"].as_u64().unwrap();
let check = |session: &Session| {
let result = session.model.result(id).unwrap();
assert!(result.error.is_none());
assert!(result.warning.as_ref().is_some_and(|warning| warning.contains("Boolean retry:")), "{result:?}");
assert!(
result.boolean_diagnostics.events.iter().any(|event| event.strategy == BooleanStrategy::CommonBore),
"{:?}",
result.boolean_diagnostics
);
result.boolean_diagnostics.clone()
};
let original = check(&session);
let expected = (10. + slope * 50.) * (8000. - PI * 100.) + PI * 300. * 5.;
assert!(rel(volume(&mut session), expected) < 2e-5);
let inspected = run(&mut session, "timeline.inspect", json!({"feature":id}));
assert_eq!(inspected["boolean_diagnostics"], serde_json::to_value(&original).unwrap());
session.refresh();
assert_eq!(check(&session), original);
run(&mut session, "edit.undo", json!({}));
run(&mut session, "edit.redo", json!({}));
assert_eq!(check(&session), original);
let reopened_doc = solidcraft_doc::Document::from_json(&session.doc.to_json()).unwrap();
let reopened = Session::new(reopened_doc);
assert_eq!(check(&reopened), original);
// A later independent feature must not inherit the earlier retry's warning.
run(&mut session, "solid.box", json!({"corner":[200,0,0],"length":5,"width":5,"height":5}));
let last = session.model.results.last().unwrap();
assert!(last.warning.is_none());
assert!(last.boolean_diagnostics.events.is_empty());
}
/// A barrel joined under a plate's back edge, its ends flush with the sides of the leaf it hangs
/// from (#827 without its fillets): the join is exact, so the command's reply carries no Boolean
/// retry, and the barrel's ends stay in the leaf's sides (it is not nudged to make the join go).
#[test]
fn a_barrel_joined_between_the_sides_of_a_leaf_needs_no_retry() {
let mut session = Session::default();
run(&mut session, "solid.box", json!({"length": 40, "width": 20, "height": 6, "corner": [0, 0, 5], "body_name": "Plate"}));
run(&mut session, "solid.box", json!({"length": 14, "width": 10, "height": 6, "corner": [10, 10, 0], "operation": "join", "targets": ["Plate"]}));
let reply = run(
&mut session,
"solid.cylinder",
json!({"radius": 5, "height": 14, "axis": "X", "base": [10, 20, 0], "operation": "join", "targets": ["Plate"]}),
);
assert!(reply["warning"].is_null(), "{reply}");
let result = session.model.result(reply["feature"].as_u64().unwrap()).unwrap();
assert!(result.boolean_diagnostics.events.is_empty(), "{:?}", result.boolean_diagnostics);
let state = session.model.state();
let plate = &state.body("Plate").unwrap().body;
assert_eq!(plate.shell_count(), 1);
assert!(plate.validity().is_empty(), "{:?}", plate.validity());
// Every edge ends on a level along x of the plate, the leaf or the barrel.
for edge in plate.edges(1e-3).unwrap() {
for p in [edge.points.first(), edge.points.last()].into_iter().flatten() {
assert!([0., 10., 24., 40.].iter().any(|x| (p.x - x).abs() < 1e-9), "an edge ends at {p:?}");
}
}
// A quarter of the barrel lies in the leaf.
let expected = 5500. + 0.75 * PI * 25. * 14.;
let actual = plate.mesh(&solidcraft_kernel::MeshTolerance::chord(1e-4)).unwrap().measure().volume;
assert!(rel(actual, expected) < 1e-5, "{actual} vs {expected}");
}
-1
View File
@@ -48,7 +48,6 @@ fn rim_cut_after_the_round_on_the_other_end() {
let id = cut_window(&mut after)["feature"].as_u64().unwrap();
let result = after.model.result(id).unwrap();
assert!(result.error.is_none() && result.warning.is_none(), "{result:?}");
assert!(result.boolean_diagnostics.events.is_empty(), "{:?}", result.boolean_diagnostics);
// The window's three faces (two sides and the top), at both points of the timeline.
assert_eq!(faces(&before), f1 + 3);
assert_eq!(faces(&after), f2 + 3);
+4 -53
View File
@@ -112,60 +112,11 @@ changes to body grouping can therefore change the generated design. This preserv
original geometric reference independently of those choices. These checks supplement the
snapshots' stricter repeatability comparisons of raw topology and tessellated measurements.
## Successful Boolean retries
## Booleans
When an accepted Boolean result needs a retry that can make it differ from the
exact one (a tool nudge, or the unverified shortcut), the feature shows a
warning in the timeline. Every other retry works on temporary geometry or
tolerances and leaves the exact result, so it is recorded but not warned about.
`timeline.inspect` and `doc.inspect` expose every retry in the feature's
`boolean_diagnostics`, including the strategy and the applied translation,
rotation, face offset and tessellation tolerance in millimetres/radians.
Candidates those retries discarded before measuring them are listed in its
`rejected`, with the reason: a candidate with an edge further outside the
operands than 5% of their size cannot be their union, cut or intersection,
and meshing one (a trim loop running metres along a cone's continuation)
could take minutes or never finish (#3020).
A common shift of both operands is reversed on the result. Coincident-face
separation and bore offsets describe temporary working geometry; they are not
claims about the final model's error. A coincident face is separated only
where the space it moves through holds what the Boolean leaves there anyway
(nothing of the target for a cut's face, the target's material for a join's);
a face with no such room stays, and the exact planar fallback is tried first.
A tool nudge changes the relative operand
placement and reports both its translation magnitude and rotation angle. When it
fails, or leaves a sliver face (narrower than 1/200 of the part), it is tried the
other way too: one way can push a face into a curved face it only touched (a cut
whose end lies tangent to a round side) and leave a sliver of it, where the other
way parts them (#897, #827).
Exact planar and piecewise fallbacks report zero displacement. When a conical
cutter meets a body with earlier conical faces (pointed bores copied in a pattern),
two finer, unshifted passes are tried first, at 1e-5 and 1e-6 of their size (never
below 1e-6 mm). A coarse first cut can leave tiny junctions that later copies cannot
intersect, so refining only the failing copy is too late. Every candidate must pass
the volume, membership and spatial checks; a rejected candidate continues to the
next tolerance, then the ordinary routes. The existing acceptance checks still
apply to all results.
A join or a cut that needed any retry is then held to what it can do at all
(`crates/kernel/src/boolean_guard.rs`, #616): the result is closed, a join
never loses volume and a cut never gains any, and outside the tool's box the
result's surface is the target's and the target's is the result's (a join or
a cut can only change the target where the tool is). A cut may come out empty
only when the tool covers the target. A result that fails is refused with an
error naming what was wrong and where, rather than returned: a wrong result
that looks right is worse than an error.
Reports belong to the evaluated feature and survive cached rebuilds and
undo/redo. Cold reopening regenerates them from the embedded design. Failed or
discarded retry branches do not contribute successful-retry events. Reports
are capped at 1024 events per feature and explicitly identify truncation.
Fuzz JSON and the text report count successful retry events by strategy for
designs and Boolean identity checks separately, including nested operations.
These are event counts, so a single Boolean or seed can contribute several.
Truncated reports are counted separately rather than implying complete totals.
Booleans run in SMLib, with no retries or fallbacks: a Boolean SMLib cannot do exactly is an
error that names why, never a nudged or approximate result. The retry reports truck's Booleans
kept (`boolean_diagnostics` in `timeline.inspect` and `doc.inspect`) are gone with them.
## Results