MCP: let a caller find out its face/edge ids went stale

snaporca-rgbj measured the damage: four chamfers on a box remove
0.400/0.397/0.397/0.395 mm3 when each id is re-read, and
0.400/0.008/0.397/0.280 when the four ids are captured up front. The kernel is
right in both runs — the second one asks for the wrong edges. Neither errors,
because a stale id still resolves to a real edge, just not the one that was
measured.

That makes it an API problem rather than a script bug. Reading the scene once and
then issuing several operations is the natural way to drive a socket, it is what
every agent will write, and it produced silently wrong geometry with nothing
anywhere reporting it.

CadDocument::topo_generation is bumped where the bodies are replaced — the single
line in recompute() where the face and edge maps actually change, so a feature
type added later cannot forget to bump it, which a per-mutator counter would
invite. describe_scene and query_topology return it. A caller may pass it back as
"generation" on any call, and a mismatch is refused with a message that says what
to do about it.

Two deliberate choices:

OPTIONAL, not mandatory. Every existing script keeps working unchanged; passing
the generation is what buys the guarantee. Making it required would break every
caller to fix a mistake only some of them make.

CHECKED AT THE DISPATCHER, not in each handler. One site covers fillet, chamfer,
shell, draft, coordsys, thicken, cut, project, delete_face and everything added
after them. A per-handler check is a list that goes stale the first time someone
adds a method in a hurry.

Not serialized: an id means something only within the run that produced it, so
persisting the counter would promise a stability the ids themselves do not have.
No recipe version change.

describe_tools now carries an id_lifetime note, because the guard only helps a
caller who knows to ask for it.

Kernel suite 167 cases / 2277 assertions green; libslic3r_gui builds. The guard
itself is NOT exercised — it needs the socket, so it is on snaporca-bdco.
snaporca-o1l2.
This commit is contained in:
Tommaso Bianchi
2026-08-12 23:27:11 +02:00
parent 9125e0b4f8
commit a3398c6609
3 changed files with 57 additions and 1 deletions
+5
View File
@@ -3404,6 +3404,11 @@ bool CadDocument::recompute()
}
}
bodies = std::move(built);
// The face and edge maps have just been rebuilt, so every global id handed out before this
// point now means something else. Bump here rather than in each mutator: this is the single
// line where the topology is actually replaced, so it cannot be forgotten by a new feature
// type the way a per-mutator bump would be.
++topo_generation;
// Face-drift fingerprint for FaceAndDirection CoordSys connectors. This runs AFTER the
// bodies are final and APPENDS to mate_conflicts (detect_mate_conflicts() cleared it at
+10
View File
@@ -607,6 +607,16 @@ public:
// - bump this whenever CadFeature::save/load gains or loses a field
// - v1 blobs are deliberately not loadable; there is no migration path by design
// - append fields ONLY at the end of save/load, never reorder (golden fixture enforces this)
// Bumped every time the bodies are rebuilt, i.e. every time the face and edge MAPS change.
// Global face/edge ids are indices into TopExp::MapShapes and mean nothing across a rebuild,
// so any caller holding an id from an earlier state is holding a wrong one. This is the
// handle that lets it find out instead of silently addressing the wrong edge.
//
// Session-scoped and deliberately NOT serialized: an id is only meaningful within the run
// that produced it, so persisting the counter would imply a promise across loads that the
// ids themselves cannot keep.
uint64_t topo_generation{1};
// v4: coordsys_face_kind + coordsys_face_edges appended (connector face-drift fingerprint).
// The bump is not optional. deserialize_recipe() gates on v == VERSION and then reads a FLAT
// symmetric field list, so a v3 blob under a v3 build that has grown two fields passes the