feat(doc): persistent names follow the kernel's exact face history (#136)

Why: every reference a user makes to a face or edge (fillet and chamfer
edges, sketches on faces, shell/draft/offset faces, joints, appearances)
is stored as a name and found again after any upstream edit. Those names
were re-derived by sampling points of each face and voting, and pieces
of a split face were numbered by where their centres lay. After an
upstream edit that splits, merges, mirrors or shrinks a face, a fillet
or sketch could silently re-attach to the wrong face, and nothing told
the user. The kernel now reports exact history for its operations, so
names follow it and fail loudly instead of guessing.

- crates/doc/src/lineage.rs: a feature's kernel histories composed into
  one lineage over nameable operands (bodies before the feature, moved
  copies, shell cavities, tools named by role: sketch curve, cap, box
  side, imported face). Instrumented: extrude/revolve/sweep/loft tools,
  join/cut/intersect, lumps after a cut, fillet/chamfer, shell, draft,
  split face/body, pattern/mirror (bodies and features), combine,
  boundary fill, move/scale, offset/delete face, thread, import.
- crates/doc/src/history_names.rs: names from lineage; split pieces told
  apart by the faces only they touch (F2:top#[F2:side:l4,F4:side:l4]),
  merged faces answer to every name (aliases), lump bodies ordered by
  their faces' names instead of shell order.
- crates/doc/src/name_lookup.rs, piece_names.rs: references resolve by
  exact name, alias or bounding faces, else nearest piece with a
  warning; names worked out from geometry (operations without history)
  are reported when only geometry decided.
- Format 14: names stored by older formats get a legacy mark and are
  found as the build that saved them found them (some geometric names
  name a face history names differently: a tube's outer wall was named
  after its inner circle). New corpus file saved by 9a979cf.
- kernel: Body::lumps carries each piece's history over the whole.
- Adversarial tests: split, merge, mirrored twins, shrink to a sliver,
  moving cut with body pieces, history-less bodies, certified sketch.
- docs/book/src/dev/naming.md describes the real mechanism and why.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
Iraklis KourtisandClaude Opus 5.5 authored and bherbruck committed 2026-10-12 11:52:53 -04:00
1 parent 2bda050872
commit 08cf526fe2
29 files changed
+2917 -322

No files matched your search

+91 -17
View File
@@ -1,22 +1,96 @@
# Persistent naming
A fillet stores the edges it rounds, and a sketch stores the face it sits on. If those were face
and edge *indices*, an upstream edit that adds a face would move the fillet to the wrong edge.
SolidCraft stores **names** instead (`crates/doc/src/naming.rs`):
A fillet stores the edges it rounds, a sketch the face it sits on, a shell the faces it opens.
If those were face and edge *indices*, an upstream edit that adds a face would move the fillet
to another edge. SolveCraft stores **names**, and finds them again every time the timeline
evaluates.
- A face of an evaluated body is named after how it was made. A face lying on a face of the
feature's input keeps that face's name; a face new to the feature is named by its role:
`F<feature>:side:<sketch curve>`, `F<feature>:start` and `:end`, `F<feature>:blend:<k>` and
so on.
- Pieces of a split face get `#1`, `#2`, … in order along the longest axis.
- An edge is named by the two faces it separates.
- Pattern and mirror copies are named after their sources plus the instance.
## Why names follow the kernel's history
Names are computed lazily, the first time something asks, from the body, the feature that made
it and the model just before that feature. Face provenance from the kernel (which input face an
output face came from) does most of the work.
Names used to be worked out from geometry: sample points of each new face were looked up on the
faces of the feature's input, and the majority won; pieces of a split face were numbered by where
their centres lay. That guess is right most of the time, and wrong exactly when it matters: after
an upstream edit that splits, merges, mirrors or shrinks a face, points can land on a neighbour,
centres can swap order, and a fillet or a sketch quietly re-attaches to a different face. Nothing
tells the user, which is the worst thing a CAD tool can do.
When a feature evaluates, each stored reference is resolved by name. If no face has the name
any more, the reference falls back to the geometry it recorded (a point on the face, a normal),
and the feature shows a warning so the user can check it. A reference that matches nothing is
an error, never a guess.
The kernel now reports, for every operation, what each face and edge of its result came from
(`crates/kernel/src/history.rs`: a face's origins and how it changed, an edge's origin edge or
the faces whose meeting made it). Names follow that history, so they are exact and deterministic,
and when a reference can no longer be found exactly the feature says so instead of guessing.
## How a body's faces are named
`crates/doc/src/lineage.rs` composes the histories of the kernel operations a feature runs (a
tool built from a sketch, cut from each target, the cut body split into lumps) into one
**lineage** over operands that can be named:
| Operand | Its faces are named |
|---|---|
| a body of the model before the feature | as they were |
| a moved copy of one (pattern, mirror) | `<name>@F<feature>.<instance>` |
| the cavity side of a shell grown outward | `F<feature>:inner:<name>` |
| a body the feature built | by role: `F<feature>:side:<sketch curve>`, `start`/`top`/`bottom`/`end` caps, `+x`… box sides, `face:<k>` imported faces |
| a copied tool of a patterned feature | `F<feature>:<role>@F<pattern>.<instance>` |
`crates/doc/src/history_names.rs` names each face of the result from its origins: a kept or
trimmed face keeps its origin's name, a fillet or chamfer face is `F<feature>:blend:<edge>` of the
edge it replaces, a shell's inner wall `F<feature>:inner:<face>`. A side's sketch curve is the
curve the profile segment lies on exactly; nothing is sampled.
- **Pieces.** Faces that end up with one name are pieces of one face, and are told apart by the
faces around them that the others lack: `F2:top#[F2:side:l4,F4:side:l4]` is the piece of the
block's top bounded by its left wall and the slot's left wall. That survives the slot moving,
growing or shrinking. Pieces nothing around them tells apart are numbered (`#1`, `#2`): by what
they came from when that differs (the two halves of a circle's wall), else in kernel order and
then unverified.
- **Merged faces.** A face made of several (a join flush with the body) takes the name of the face
they are all pieces of, else the target's; it answers to the others' names too (aliases).
- **Edges** are named by the faces they separate, `<face A>|<face B>`, `~k` when several edges run
between the same two faces (numbered by where they lie; a reference by such a number is
reported when only geometry confirms it).
- **Lumps.** When a cut leaves a body in pieces, the whole is named first, so pieces of one face in
different bodies are told apart, and the bodies (`Bar`, `Bar (1)`) are assigned in the order of
their least face name, not the kernel's shell order (which an upstream edit can swap).
Names are worked out lazily, on first request, and shared by a body's clones.
### Where there is no history
A body made by an operation that does not report history yet (some kernel paths return none; see
the kernel's handoff lists, and the doc features that do not compose it yet: sheet metal, plastic
and surface features, emboss, rib, replace face, the faceted thread cut), or a face whose role
is not known exactly (a loft's sides, a coil's or pipe's generated profile, a hole's tool), is
named the old way, from geometry, and its names are marked **unverified** (`model.faces` reports
`exact: false`). A reference that resolves through an unverified name while its stored point no
longer lies on the face says so: `face ... was found by geometry, not exact history; check it`.
## How a reference finds its face again
`crates/doc/src/name_lookup.rs`, in order:
1. the face with that name;
2. the one face answering to it: merged into it (the feature says it was merged), or the named
piece, which the faces listed in its name still bound;
3. the pieces of it (it was split, or it is on several bodies): the one nearest the reference's
point, and the feature says it was split;
4. nothing: the feature falls back to the geometry it recorded and says the face no longer exists.
An edge name is found as the edge between the two faces it names, each found as above: when
exactly one edge runs between them it is that edge, even after a split of one of its faces.
## Older designs
Designs saved before format 14 stored names worked out from geometry, and a few of them name a
face that history names differently (a tube's outer wall was named after the sketch curve nearest
its centre, its inner circle). Migrating such a design marks those names (`?F2:top#2`), and they
are found among the names geometry gives, as the build that saved them found them: an older design
evaluates as it was saved. Picking the face again stores its exact name.
## Not done yet
- Certificates (`FaceSource`, the strict attachment identities behind sketches on faces and
configured face styles) still travel only where the kernel's planar-source mapping keeps them;
carrying them through `History::face_sources` would let a certified attachment follow a face a
later join reshapes, which today is refused on purpose.
- Operations without history, listed above, keep the unverified geometric names.