* Persistent naming: a proposal from a second implementation A design proposal for the persistent-naming failures still open (#4341, #5198, #5308, #5857, #5858, #5860, #5861), from building persistent naming in Form Studio: fall back by lineage, never by geometry alone; a suppressed maker is not a lost reference; names saved as structured one-level values; ordinals from the feature, not the kernel's face order; split picks resolve to all pieces; body identity apart from the display name. Documentation only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Persistent naming proposal: body identity follows the #5308 plan Change 6 now agrees with bluecmd's plan on #5308: an id minted from next_id and stored per feature slot, better than the derived identity the first version proposed. It adds that a stored id is only as stable as the slot assignment, which today follows each lump's least face name, and gives the lineage signature from the #5308 discussion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
14 KiB
Persistent naming: what a second implementation suggests
Status: proposal, not adopted. Nothing here is built in SolidCraft. Related: #4341, #5198, #5308, #5857, #5858, #5860, #5861, #23.
This note comes from building persistent naming for Form Studio, a T-spline modeling tool with its own feature timeline, rollback marker, suppression, and editing of earlier features. Its kernel is a control mesh, not a B-rep, so its entities are the vertices, edges and faces of a cage. A feature's picks still have to survive the same upstream edits SolidCraft's do.
Form Studio is LGPL-2.1. Rule 2 of CONTRIBUTING.md says not to read it, so this note links none of its code and contains none of it. What follows is method and measured results, written by the code's author and offered under MIT OR Apache-2.0. No commercial CAD was driven or read for any of it.
What SolidCraft already gets right
The hard parts are done. Names follow the history each kernel operation reports (#136), not
sampled geometry. Faces are named by their role in the feature that made them (F2:side:l4).
Pieces are told apart by the faces around them, a merged face still answers to its old names, and
a feature says when a reference was split or found only by geometry. Form Studio follows the same
rules.
The failures still open sit in three places: what happens when a name is gone, how a name is written down, and where an ordinal comes from. Six changes follow, the most harmful failure first.
1. When a name is gone, fall back by lineage, never by geometry alone
SolidCraft already has this rule for one kind of reference. A sketch's link to a projected edge
whose name is gone "keeps its last geometry ... it does not take whichever edge is nearest now"
(docs/book/src/dev/naming.md). Blends don't follow it yet. Read at 49c0faa, not run:
- A blend edge whose name no longer resolves falls back to "the edge at the picked point"
(
crates/doc/src/eval.rs:4128). - A blend with no stored body takes the body with the nearest edge (
blend_target,eval.rs:3951), andnamed_body(eval.rs:3997) ends with the nearest candidate.
Either path can put a fillet on an edge of an unrelated body. That is #4341: suppress Extrude1, or move Sketch1 500 mm, and Chamfer1 lands on Body2. The warning is honest, but the geometry is wrong, and every later feature builds on it.
Form Studio resolves a stored reference with the first of these four steps that finds something:
- Exact. The entity with that name.
- Pieces. The name is gone because an earlier feature split the entity. The result is every live piece its history leads to, including pieces that were split again (see change 5).
- Lineage guess. Nothing was split from it, but its maker rebuilt it differently, for example a subdivision changed from 2 × 2 to 3 × 3. A candidate must come from the same feature and share either the role or at least one origin with the stored reference. Scoring starts at 4 for the same feature, then adds 2 for the same role, 2 for each shared origin and 1 for the same ordinal. Distance to the stored point only breaks ties. The result is one entity, never every candidate that matches.
- Lost. No candidate. The pick resolves to nothing, the feature says so, and its other picks still apply. A feature with every pick lost is in error, and the document passes through it unchanged.
What step 3 leaves out matters most: no entity made by another feature can stand in, however close it is. In SolidCraft terms, a stored reference keeps one level of what its name was made from (feature, role, the names of its origins, ordinal). After the name is lost, the lookup searches the rebuilt faces that feature made. It doesn't search space. Change 3 describes where that recipe is kept.
The stored point stays useful in three ways: it breaks ties, it raises the existing "found by geometry, not exact history; check it" note, and it is the only key for references saved without a name. It should never choose a body or an edge on its own. Freezing a lost reference at its last geometry, as sketches already do, is fine, because nothing new gets picked.
Measured in Form Studio, before names existed, when picks were raw indices (2026-10-05, 3 scripted histories, 18 features):
| Test | Result |
|---|---|
| Replay from the start | byte-identical to every recorded state |
| Edit earlier features' numbers, topology unchanged | all 33 later features on the same entities |
| Insert a subdivision upstream | of 60 later features: 0 kept exactly, 19 kept by position, 25 refused, 16 ran on a different entity with no error |
The 16 silent wrong results came from indices that now pointed at other entities. An index, like a picked point, records where something was, not what it was. After names were built, six rebuild tests pin the rules down. All six pass today:
- An unchanged rebuild finds every pick exactly.
- A subdivision inserted before a move of the face it splits: the move takes every piece.
- A feature inserted elsewhere leaves every later name unchanged.
- Deleting the feature that made a picked face leaves the later feature in error, with the document passing through it.
- A subdivision changed from 2 × 2 to 3 × 3: a move of its corner piece moves the new corner piece.
- A loop inserted before a crease of the edge it cuts: both halves are creased.
We have not re-run the 60-feature insert test since names were built.
2. A suppressed maker is not a lost reference
When the feature that made a referenced face or edge is suppressed, or sits past the rollback marker, the reference isn't lost. It is waiting. Form Studio reads the maker from the feature id in the stored recipe:
- Maker suppressed. The pick stays on the dependent feature and is shown as suppressed. That feature is not in error, and nothing is re-pointed. Unsuppress the maker and the pick resolves exactly again.
- Maker deleted. The pick is shown as missing. The user can clear missing references or pick again.
Neither case falls back to geometry. SolidCraft names already start with their maker (F2:...),
so it can tell these cases apart with no new data. #4341's first repro (suppress Extrude1) is this
case.
3. Store a name as data, and keep the text for people
#5857 and #5858 are problems with the text form: where a nested name's suffix belongs, and an id
that contains |. #5861 plans a better grammar for both. The text form has one more hazard, read
from source and not run. later_suffixes (crates/doc/src/persistent_name/parser.rs:74) decides
which face a piece qualifier belongs to by comparing feature numbers (*f > feature). Feature
numbers follow creation order (Document::next_id), and timeline.reorder moves a feature
without renumbering it. After a reorder, the same text can therefore be read the other way.
Form Studio never parses a name. This would take two steps here, each useful on its own:
- Save
FaceNameandEdgeNameas structured values, not text.persistent_name.rsalready reads names into a tree; the design file would keep that tree instead of a string. A tree has no ambiguous suffix (#5857), a curve id is a field and can hold|(#5858), and no rule has to compare feature numbers. #5861's migration then happens once: read old text with today's rules and write the tree. The parenthesized text becomes the display form formodel.faces, MCP and messages. A command given that text looks it up in the live model, and nothing parses text back into an identity. - Make every name one level deep. In Form Studio, each entity's name is a fixed-length token
derived from a one-level recipe: feature, kind, role, origins (the tokens of its parents) and
ordinal. The recipe stays in the body's name table while the entity lives. A stored reference
keeps the token, the recipe and the picked point. Depth lives in the table: a piece of a piece
of a blend is a chain of one-level records. A stored reference is then the same size however
many splits came before it (#5860; the
MAX_NAME_LENcomment notes that a face engraved with text passes 4 KiB today). The recipe is also the data change 1's lineage guess needs.
Form Studio uses a 53-bit hash and redraws on a collision within a mesh. A Rust version needs a
hash whose output is fixed across platforms and releases (not std's DefaultHasher), and 64
bits or more.
4. Ordinals come from the feature, never from the kernel's face order
In #5198 the same design names a face F3:bottom#1 on one kernel and F3:bottom#2 on the other,
so the reference picks the other cap. The #k that breaks a remaining tie follows the kernel's
face order (naming.md: "else in kernel order and then unverified"). We suggest two rules:
- No two faces of one feature share a role and origins. When two do, the role table is too
coarse. An extrude's two caps are
startandend, whichever way it points. - A real tie gets its ordinal from the feature's own definition. For example, a closed curve's wall split by a seam is ordered by the curve parameter where each piece starts, and caps along a direction by their distance along it. The order a kernel returns faces in is never used.
In Form Studio the ordinal counts what one edit made with the same feature, role and origins, in
the tool's own construction order. Form Studio has only one kernel, so we set this rule in advance
and never saw it fail. A gate for it: build each design in the naming tests on both kernels and
compare every name. The kernel switch (#3992, #4012) needs that gate, and it lives in the
document and engine tests, not in crates/kernel/.
5. A split pick resolves to all its pieces
When a named face or edge was split, SolidCraft takes the piece nearest the stored point
(name_lookup.rs; locate_edge: "the piece of it nearest near"). Form Studio takes every
piece. The user picked the whole entity, and after a split the whole entity is its pieces:
- A fillet on an edge that an upstream slot now interrupts should round both remaining runs.
- A shell that opened a face should open both halves.
A consumer that needs exactly one entity, such as a sketch plane or a joint, takes the piece nearest the stored point from that list and says so, as it does today.
This changes what some saved designs rebuild to, so it raises the Changes a default question
(see Questions below).
6. A body's identity is not its name
In #5308, references to a body use its display name. Features already keep the two apart: a
minted id (F12) and a name the user edits. Bodies can do the same, and bluecmd's plan on #5308
does it: an id minted from next_id and stored with the feature slot by slot, with the name
becoming a label. That is better than deriving the identity from the creating feature and the
lump's place, as an earlier version of this note proposed, because a stored id survives later
changes to how lumps and faces are named. Form Studio does the same for features: the id is
minted once and kept through renames, and a rename never rewrites a reference.
A stored id is only as stable as the step that decides which lump fills which slot. Today the
pieces of a split body are assigned in the order of their least face name
(crates/doc/src/lineage.rs:554 at 33281cf), and text order says nothing about which piece is the
original. Suppose an upstream edit makes a cut slice a small piece off a body, and that piece's
names sort first. The piece then takes the body's id, and with it the material and appearance.
Assigning slots by lineage keeps the id on the main piece:
- Each face contributes its own name and the names of the faces it was split from.
- When a slot is pinned, the feature stores a MinHash signature of that set beside
body_names. - On evaluation, each slot takes the lump whose signature overlaps its own the most.
The details are in the #5308 discussion.
Measuring it
Classify every reference resolution the engine performs: exact, merged, pieces, lineage guess,
geometry only, or lost. "Geometry only" is the class that can be silently wrong. Count it in the
naming tests (crates/engine/src/naming_history_tests.rs, from #23) and in the action-matrix
corpus, and ratchet it down to zero for references that have a name. The count changes no
behavior, so it can land first and show what each later change buys.
Order of work
Each step is a small PR on one topic:
- Classify and count resolutions. No behavior change.
- Suppressed versus missing (change 2; #4341, first repro).
- Lineage fallback for blend edges, then for face references (change 1; #4341, second repro).
- Structured names, together with #5861's step 2 (change 3).
- Cap roles and ordinals from the feature (change 4; #5198).
- Body identity (change 6), which bluecmd plans on #5308, then slots assigned by lineage.
Steps 3, 4 and 6 save something new in a design, so each adds a file under
crates/doc/src/format/pending/. Steps 3 and 5 change what some saved designs rebuild to.
Questions for maintainers
- Where today's point fallback lands on some edge, a lost reference would apply nowhere and the feature would report it. Is that acceptable for saved designs, or should it sit behind a preference?
- Should names be saved as structured values before #5861's step 2, or instead of it?
- Should a split pick resolve to all its pieces by default?
Background
- J. Kripac, "A mechanism for persistently naming topological entities in history-based parametric solid models", Computer-Aided Design 29(2), 1997.
- V. Capoyleas, X. Chen, C. M. Hoffmann, "Generic naming in generative, constraint-based design", Computer-Aided Design 28(1), 1996.
- D. Marcheix, G. Pierra, "A survey of the persistent naming problem", Proceedings of the Seventh ACM Symposium on Solid Modeling and Applications, 2002.
What was read and what was run
SolidCraft was read at 49c0faa (2026-10-28 UTC). It was not built or run, so the mechanisms given for
#4341 and for reordering come from reading the source. Form Studio's six rebuild tests were run on
2026-10-27 and all pass. The insert measurement is from 2026-10-05, before names existed.