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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
69 lines
3.4 KiB
Rust
69 lines
3.4 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn piece_names_parse_into_stem_and_bounding_faces() {
|
|
use crate::naming::{parent_piece, strip_edge_piece, strip_piece};
|
|
use crate::piece_names::piece_parts;
|
|
assert_eq!(strip_piece("F2:top#[F2:side:l4,F4:side:l4]"), "F2:top");
|
|
assert_eq!(strip_piece("F2:top#[a]#[b]"), "F2:top");
|
|
assert_eq!(parent_piece("F2:top#[a]#[b]"), Some("F2:top#[a]"));
|
|
assert_eq!(strip_piece("F2:top#[a]#2"), "F2:top");
|
|
assert_eq!(strip_piece("F2:top#2"), "F2:top", "an older design's numbered piece");
|
|
assert_eq!(strip_piece("F2:top"), "F2:top");
|
|
assert_eq!(parent_piece("F2:top"), None);
|
|
assert_eq!(piece_parts("F2:top#[F2:side:l4,F5:blend:A#[x,y]|B]"), Some(("F2:top", vec!["F2:side:l4", "F5:blend:A#[x,y]|B"])));
|
|
assert_eq!(piece_parts("F2:top#[]"), Some(("F2:top", vec![])));
|
|
assert_eq!(strip_edge_piece("F2:side:l2|F2:top#[F4:side:l2]~2"), "F2:side:l2|F2:top");
|
|
assert_eq!(strip_edge_piece("F2:side:l2#1|F2:top#2"), "F2:side:l2|F2:top");
|
|
// Hostile names never panic and are their own stems.
|
|
for n in ["", "#", "#[", "]", "a#[", "a]", "#[]", "a#[b", "x#[[]]]", "é#[ü]", "#1", "a#[b]#", "~", "a~"] {
|
|
let _ = (strip_piece(n), parent_piece(n), piece_parts(n), strip_edge_piece(n));
|
|
}
|
|
assert_eq!(strip_piece("#[]"), "#[]");
|
|
}
|
|
|
|
fn named(names: &[&str]) -> Vec<Named> {
|
|
names.iter().map(|n| Named { name: n.to_string(), aliases: Vec::new(), verified: true, origins: None }).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn pieces_are_told_apart_by_the_faces_only_they_touch() {
|
|
// A top face cut in two by a slot: each piece touches the front and back walls (common),
|
|
// its own end wall and its own slot wall.
|
|
let faces = named(&["top", "top", "front", "back", "left", "right", "slot:a", "slot:b"]);
|
|
let adjacent = [(0, 2), (0, 3), (0, 4), (0, 6), (1, 2), (1, 3), (1, 5), (1, 7)];
|
|
let names = qualify(faces, &adjacent);
|
|
assert_eq!(names.faces[0], "top#[left,slot:a]");
|
|
assert_eq!(names.faces[1], "top#[right,slot:b]");
|
|
assert!(names.verified(0) && names.verified(1));
|
|
// A stored name finds its piece, though the faces around it have changed since.
|
|
assert_eq!(matches(&names, 0, "top#[left]"), Some(Match::Piece));
|
|
assert_eq!(matches(&names, 1, "top#[left]"), Some(Match::Stem));
|
|
assert_eq!(matches(&names, 0, "top"), Some(Match::Piece), "a piece of the named face");
|
|
assert_eq!(matches(&names, 0, "top#[left,slot:a]"), Some(Match::Exact));
|
|
assert_eq!(matches(&names, 2, "top"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn pieces_nothing_tells_apart_are_numbered_and_unverified() {
|
|
let faces = named(&["top", "top", "ring"]);
|
|
let names = qualify(faces, &[(0, 2), (1, 2)]);
|
|
assert_eq!(names.faces[0], "top#1");
|
|
assert_eq!(names.faces[1], "top#2");
|
|
assert!(!names.verified(0) && !names.verified(1));
|
|
}
|
|
|
|
#[test]
|
|
fn a_merged_face_takes_the_shared_stem_or_the_first_name_and_answers_to_the_others() {
|
|
let m = merged(named(&["top#[a]", "top#[b]"])).unwrap();
|
|
assert_eq!(m.name, "top");
|
|
assert_eq!(m.aliases, vec!["top#[a]".to_string(), "top#[b]".to_string()]);
|
|
let m = merged(named(&["top#1", "top#2"])).unwrap();
|
|
assert_eq!(m.name, "top", "numbered pieces joined again");
|
|
let m = merged(named(&["F2:top", "F9:top"])).unwrap();
|
|
assert_eq!(m.name, "F2:top");
|
|
assert_eq!(m.aliases, vec!["F9:top".to_string()]);
|
|
let names = qualify(vec![m], &[]);
|
|
assert_eq!(matches(&names, 0, "F9:top"), Some(Match::Alias));
|
|
}
|