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:
1 parent
2bda050872
commit
08cf526fe2
29 files changed
+2917
-322
No files matched your search
@@ -109,6 +109,22 @@ fn legacy_v9_imported_emboss_anchor_and_ordinary_face_paint_replay_without_renam
|
||||
legacy["format"] = json!("solvecraft/9");
|
||||
let reopened = Session::new(solvecraft_doc::Document::from_json(&legacy.to_string()).unwrap());
|
||||
assert_eq!(reopened.doc.feature(anchored.id).unwrap(), &anchored);
|
||||
// Names saved before format 14 keep naming the face they named, marked so they are found as
|
||||
// the build that saved them found them.
|
||||
let mark = |name: &Option<String>| name.as_ref().map(|n| format!("{}{n}", solvecraft_doc::naming::LEGACY_MARK));
|
||||
let mut saved_plane = saved_plane;
|
||||
if let solvecraft_doc::PlaneRef::Face { name, .. } = &mut saved_plane {
|
||||
*name = mark(name);
|
||||
}
|
||||
let paint: Vec<_> = paint
|
||||
.into_iter()
|
||||
.map(|mut face| {
|
||||
if !face.strict_name {
|
||||
face.name = mark(&face.name);
|
||||
}
|
||||
face
|
||||
})
|
||||
.collect();
|
||||
let FeatureKind::Sketch { plane, .. } = &reopened.doc.feature(badge).unwrap().kind else { panic!() };
|
||||
assert_eq!(plane, &saved_plane);
|
||||
assert_eq!(reopened.doc.appearances.faces, paint);
|
||||
@@ -156,10 +172,10 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance":20,"profiles":[{"point":[9.5,0]}],"body_name":"Sleeve"}));
|
||||
let names = solvecraft_doc::naming::face_names(s.model.state().body("Sleeve").unwrap());
|
||||
// Preserve the legacy centroid-family name even though strict paint uses
|
||||
// the separately certified outer source curve.
|
||||
// The outer wall is named after its own sketch curve: its history says so. (Builds before
|
||||
// format 14 named it after the curve nearest its centre, the inner circle; see below.)
|
||||
let name = solvecraft_doc::naming::face_names_at(&s.model.state(), &[Vec3::new(0., 10., 7.5)]).remove(0);
|
||||
assert!(name.contains("side:inner"));
|
||||
assert!(name.contains("side:outer"));
|
||||
run(&mut s, "sketch.create", json!({"plane":{"origin":[0,10,0],"x_dir":[-1,0,0],"y_dir":[0,0,1]},"name":"Badge"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
@@ -172,6 +188,13 @@ fn legacy_v9_annulus_outer_side_emboss_keeps_its_original_heuristic_anchor() {
|
||||
assert!((volume - std::f64::consts::PI * 380. - 21.).abs() < 0.2, "volume {volume}");
|
||||
let mut legacy: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
|
||||
legacy["format"] = json!("solvecraft/9");
|
||||
// What a version 9 design stored for that pick: the centroid-family name of the outer wall
|
||||
// piece holding the point. It keeps finding that piece, as it did when saved.
|
||||
for feature in legacy["features"].as_array_mut().unwrap() {
|
||||
if feature["id"] == json!(feature_id) {
|
||||
feature["face_names"] = json!(["F2:side:inner#4"]);
|
||||
}
|
||||
}
|
||||
let reopened = Session::new(solvecraft_doc::Document::from_json(&legacy.to_string()).unwrap());
|
||||
assert!(reopened.model.result(feature_id).unwrap().error.is_none());
|
||||
assert!((reopened.model.state().body("Sleeve").unwrap().body.tessellate_with(1e-4, true).unwrap().measure().volume - volume).abs() < 1e-8);
|
||||
|
||||
@@ -262,7 +262,10 @@ fn model_faces(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.body
|
||||
.faces(tol)?
|
||||
.iter()
|
||||
.map(|f| json!({"index": f.index, "name": names.get(f.index), "area": f.area, "centroid": f.centroid, "plane_normal": f.plane_normal}))
|
||||
.map(|f| {
|
||||
json!({"index": f.index, "name": names.get(f.index), "exact": solvecraft_doc::naming::face_name_exact(b, f.index),
|
||||
"area": f.area, "centroid": f.centroid, "plane_normal": f.plane_normal})
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"body": b.name, "faces": faces}))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user