Re-edit: list the bodies as of the feature's timeline slot, not the final ones

Found by sweeping the index-space defect class deliberately rather than by
hitting it: that class produced 4 of the 8 defects found by hand yesterday, so
it was worth auditing every combo in the panel that maps a row selection onto
a document index.

Most of it came back clean — the loft sidecar vectors are consistent at all
four read sites, the sheet-body pickers go through the helper everywhere, mate
connectors carry client data. Six did not. A stored target_body indexes the
body list AS IT WAS just before that feature ran during replay, but Transform,
Mirror, Thicken, Rib, Project and DeleteFace all populated their combo from
the live m_doc.bodies. Boolean and Cut already replayed to the right slot.

The failure is concrete: model a body, Thicken it, then Cut something later in
the tree. A Cut replaces one body with two, so every index at or after it
shifts. Reopen the Thicken and the combo lists the post-cut bodies while
selecting the pre-cut index — showing, and on confirm re-targeting, a
different body than the feature actually used. A Boolean that consumes its
tool body shifts them the other way for the same result.

fill_body_choice() does the truncated replay populate_body_choices() already
did, for the single-combo tools. Six call sites, and 60 lines of duplicated
population loops go with them.

Visible change when testing: re-editing an early feature now lists FEWER
bodies, because it lists only those that existed then. That is correct — you
cannot target a body that did not exist yet — and it is what Boolean and Cut
have always done.

The new kernel test pins the invariant the GUI now leans on: a Cut turns one
body into two, and replaying to just before it yields the earlier, shorter
list. If body ordering after a split ever changes, that assumption fails
loudly here instead of silently in a dialog.

NOT click-tested — GUI wiring, compile-verified only. Filed as snaporca-oz7
and added to snaporca-cfi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-07-26 09:32:47 +02:00
co-authored by Claude Opus 5
parent 8f06b9dfd8
commit a95e8ee701
3 changed files with 70 additions and 60 deletions
+36
View File
@@ -1408,6 +1408,42 @@ TEST_CASE("draft tapers a solid face about the body base", "[CadDocument]")
REQUIRE_FALSE(bad.recompute());
}
TEST_CASE("a split renumbers the bodies a later feature indexes", "[CadDocument][cut]")
{
// Pins the invariant the Design tab's re-edit path depends on (snaporca-oz7): a stored
// target_body indexes the body list AS IT WAS when that feature ran, and a Cut placed
// later in the tree changes that list. If this test ever fails, the GUI's
// fill_body_choice() replay-to-timeline-slot assumption needs revisiting with it.
using Catch::Matchers::WithinRel;
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Box");
doc.add_extrude(sk, 20.0, false, BooleanMode::New, "Ext");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 1);
// Cut the single body in half, keeping BOTH pieces — what the Cut card always does.
doc.add_cut(SketchPlane::XY(), 10.0, false, true, true, 0, "Split");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
REQUIRE(doc.bodies.size() == 2); // one body became two
// Body index 1 did not exist before the Cut. A feature recorded BEFORE the Cut could
// never have referred to it, which is exactly why re-edit must list the earlier set.
const double half = 20.0 * 20.0 * 10.0;
double v0 = double(SketchEngine::tessellate(doc.bodies[0].shape).volume());
double v1 = double(SketchEngine::tessellate(doc.bodies[1].shape).volume());
CHECK_THAT(v0 + v1, WithinRel(2.0 * half, 1e-3));
// Replaying to just before the Cut yields the pre-split list — the one a feature sitting
// there indexes into. This is the operation fill_body_choice() performs.
CadDocument as_of = doc;
as_of.features.resize(as_of.features.size() - 1); // drop the Cut
REQUIRE(as_of.recompute());
REQUIRE(as_of.bodies.size() == 1);
CHECK(as_of.bodies.size() < doc.bodies.size());
}
TEST_CASE("cut splits a body with a plane", "[cut]")
{
using Catch::Matchers::WithinRel;