CAD re-edit: mid-timeline rebuild tests + non-silent Edit fallback

Mirror of snaporca-cad a82cb0d. snaporca-88g — three [CadDocument] tests
proving recompute() replays the whole timeline, so editing any feature (not
just the last) rebuilds downstream: mid-timeline extrude edit -> fillet
rebuilds; first-feature sketch edit propagates the chain; the same survives
serialize_recipe()/deserialize_recipe(); a sketch->extrude->hole->chamfer
chain rebuilds hole+chamfer on a mid-edited extrude. All 3 pass (34 assertions,
Catch2 v3). on_edit_feature edits the tree-selected feature at any position
(13/16 types); Import/Boolean/Cut get a status message instead of a silent
no-op (full dialogs = follow-up snaporca-nu9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-06-29 06:30:01 +02:00
co-authored by Claude Opus 4.8
parent a121e30a13
commit b355d04d3a
2 changed files with 102 additions and 1 deletions
+7 -1
View File
@@ -4498,7 +4498,13 @@ void DesignPanel::on_edit_feature()
load_feature_into_dialog(f);
open_tool(Tool::Draft);
break;
default: break;
default:
// Import / Boolean / Cut have no parametric edit dialog yet (follow-up
// snaporca-nu9). Don't silently swallow the Edit click — tell the user.
m_status->SetForegroundColour(wxNullColour);
m_status->SetLabel(_L("This feature type can't be edited yet"));
m_status->Refresh();
break;
}
}
+95
View File
@@ -1421,3 +1421,98 @@ TEST_CASE("deserialize_recipe rejects future version", "[CadDocument]")
}
REQUIRE_FALSE(doc.deserialize_recipe(oss.str()));
}
TEST_CASE("re-edit: editing a mid-timeline feature rebuilds downstream", "[CadDocument]")
{
using Catch::Matchers::WithinRel;
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(),
20, 10, 0, "Sketch1");
REQUIRE(sk >= 0);
int ex = doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude1");
REQUIRE(ex >= 0);
doc.add_fillet(1.0, FaceGroup::All, "Fillet1");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
Vec3d sz0 = doc.display_mesh.bounding_box().size();
REQUIRE(sz0.z() > 0.0);
// Edit the MID feature (extrude — NOT the last; Fillet is downstream).
doc.features[ex].distance = 12.0;
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
Vec3d sz1 = doc.display_mesh.bounding_box().size();
REQUIRE(sz1.z() > sz0.z() + 1.0);
REQUIRE(doc.bodies.size() == 1);
// Edit the FIRST feature (sketch width) — must propagate the whole chain.
doc.features[sk].width = 30;
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
Vec3d sz2 = doc.display_mesh.bounding_box().size();
REQUIRE(sz2.x() > sz1.x() + 1.0);
}
TEST_CASE("re-edit survives serialize -> deserialize", "[CadDocument]")
{
using Catch::Matchers::WithinRel;
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(),
20, 10, 0, "Sketch1");
REQUIRE(sk >= 0);
doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude1");
doc.add_fillet(1.0, FaceGroup::All, "Fillet1");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
auto blob = doc.serialize_recipe();
REQUIRE_FALSE(blob.empty());
CadDocument doc2;
REQUIRE(doc2.deserialize_recipe(blob));
REQUIRE(doc2.error.empty());
REQUIRE(doc2.recompute());
REQUIRE(doc2.error.empty());
Vec3d sz_pre = doc2.display_mesh.bounding_box().size();
REQUIRE(sz_pre.z() > 0.0);
// Mid-edit the deserialized document — the persisted recipe stays re-editable.
doc2.features[1].distance = 12.0;
REQUIRE(doc2.recompute());
REQUIRE(doc2.error.empty());
Vec3d sz_post = doc2.display_mesh.bounding_box().size();
REQUIRE(sz_post.z() > sz_pre.z() + 1.0);
}
TEST_CASE("re-edit: multi-type timeline replays all downstream features", "[CadDocument]")
{
using Catch::Matchers::WithinRel;
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(),
30, 30, 0, "Sketch1");
REQUIRE(sk >= 0);
int ex = doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude1");
REQUIRE(ex >= 0);
doc.add_hole(6.0, 4.0, false, 0.0, 0.0, SketchPlane::XY(), "Hole1");
doc.add_chamfer(1.0, FaceGroup::All, "Chamfer1");
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
size_t n_bodies = doc.bodies.size();
REQUIRE(n_bodies >= 1);
Vec3d sz0 = doc.display_mesh.bounding_box().size();
REQUIRE(sz0.z() > 0.0);
// Edit the MID extrude — downstream Hole and Chamfer must rebuild.
doc.features[ex].distance = 16.0;
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
REQUIRE(doc.bodies.size() == n_bodies);
Vec3d sz1 = doc.display_mesh.bounding_box().size();
REQUIRE(sz1.z() > sz0.z() + 1.0);
}