Files
OneCAD/crates/doc/src/history_names_tests.rs
T
Iraklis KourtisandClaude Opus 5.5 08cf526fe2 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00

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));
}