Join keeps exact face attachments on faces it leaves unchanged (#420) (#453)

A Union dropped every construction certificate, so after any join no face
could take a durable sketch: sketch.create failed with "this face has no
exact planar source" even on a face the join never touched (#420, and the
"CREATE SKETCH: operation failed" reports #344, #195).

- kernel: map planar sources through a Union from both operands, with the
  same exact-support rule Cut uses. Coplanar supports of the two operands
  carry distinct identities, so a face merged from both stays without one.
- doc: Join extrude and Join box tools are certified like new bodies; a
  cap moved off its designed height (into the target, or past a curved To
  face) is not certified. Join producers are accepted when certifying.

Tests: both operands' faces attach and follow an upstream edit and a
reopen; a merged coplanar face is refused and fixed:true works; a face a
join covers whole is lost without moving to the new roof. Tests that used
a joined face as their "uncertified" example now use a merged face; the
trimmed-face case in a_sketch_on_a_face_stays_on_that_face now stays
attached to the surviving piece.


Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Irakli KourtisandClaude Opus 5.5 authored and GitHub committed 2026-10-12 20:31:32 -04:00
1 parent 9e60080b10
commit 722bd007c8
8 files changed
+199 -41

No files matched your search

+5 -9
View File
@@ -1914,7 +1914,8 @@ mod naming {
let mut legacy = Session::new(solidcraft_doc::Document::from_json(&old.to_json()).unwrap());
assert_eq!(attached(&legacy), Some(10.));
assert!((top(&mut legacy) - 15.).abs() < 1e-6);
// The upstream join consumes the certified body; new attachments cannot follow its substitute roof.
// An upstream join that trims the certified face: the attachment stays on what is left
// of it and never follows the substitute roof (see face_tests for a face covered whole).
for target in [&mut s, &mut legacy] {
run(target, "timeline.roll_to", json!({"feature": "Block"}));
run(target, "sketch.create", json!({"plane": "XY", "name": "B"}));
@@ -1935,19 +1936,14 @@ mod naming {
};
assert!(anchor.is_none(), "opening old face references must not infer a strict attachment");
let failed = |s: &Session, name: &str| s.model.results.iter().find(|r| r.id == s.doc.find_feature(name).unwrap().id).unwrap().error.is_some();
assert!(failed(&s, "Top"));
assert!(failed(&s, "Peg"), "lost sketch cannot supply downstream geometry");
assert_eq!(attached(&s), None);
assert!(!failed(&s, "Top"));
assert!(!failed(&s, "Peg"));
assert_eq!(attached(&s), Some(10.), "the strip of Block's top left beside the boss is still that face");
assert!((top(&mut s) - 20.0).abs() < 1e-6, "{}", top(&mut s));
run(&mut s, "timeline.suppress", json!({"feature":"Boss","suppressed":true}));
assert!(!failed(&s, "Top"));
assert!(!failed(&s, "Peg"));
assert_eq!(attached(&s), Some(10.));
assert!((top(&mut s) - 15.).abs() < 1e-6);
run(&mut s, "edit.undo", json!({}));
assert!(failed(&s, "Top"));
assert!(failed(&s, "Peg"));
assert_eq!(attached(&s), None);
}
#[test]