feat(kernel): an edge's meeting sides are sets of origins (merged faces)
EdgeOrigin::Meet now names each side as a Side: the sorted, de-duplicated origins of the face there. substitute maps a side to the union of the inner faces' origins, so a meeting with a face an inner operation merged keeps its lineage; meeting() derives sides from whole faces. Fillets after box cuts that leave coplanar pieces now report exact history. Prism lineage meets a cap of several origins and resolves wall junctions away from shared vertices by the operand's one edge between the walls. bore_union's recut reports history only for a one-face bore (pieces of a two-arc bore's recut wall were given both arcs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
1 parent
08d7037176
commit
43ddde4a5d
11 files changed
+347
-76
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use super::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History};
|
||||
use super::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History, Side};
|
||||
use crate::{Body, box_solid, transform};
|
||||
|
||||
fn face(operand: usize, face: usize) -> FaceOrigin {
|
||||
@@ -53,13 +53,13 @@ fn substitute_a_body_made_from_a_sketch() {
|
||||
let mut join = History {
|
||||
operands: 2,
|
||||
faces: vec![prov(vec![face(0, 2)], Change::Modified), prov(vec![face(1, 1)], Change::Kept)],
|
||||
edges: vec![EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Meet { a: face(0, 2), b: face(1, 1) }] }],
|
||||
edges: vec![EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::meet(face(0, 2), face(1, 1))] }],
|
||||
};
|
||||
join.substitute(1, &extrude).unwrap();
|
||||
assert_eq!(join.operands, 1);
|
||||
assert_eq!(join.faces[0], prov(vec![face(0, 2)], Change::Modified));
|
||||
assert_eq!(join.faces[1], prov(vec![FaceOrigin::Profile { segment: 3 }], Change::Generated));
|
||||
assert_eq!(join.edges[0].origins, vec![EdgeOrigin::Meet { a: face(0, 2), b: FaceOrigin::Profile { segment: 3 } }]);
|
||||
assert_eq!(join.edges[0].origins, vec![EdgeOrigin::meet(face(0, 2), FaceOrigin::Profile { segment: 3 })]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -78,6 +78,13 @@ fn check_refuses_a_history_that_does_not_describe_the_body() {
|
||||
let mut short = h.clone();
|
||||
short.faces.pop();
|
||||
assert!(short.check(&a, &[&a]).is_err(), "face count");
|
||||
let mut reversed = h.clone();
|
||||
reversed.edges[0].origins = vec![EdgeOrigin::Meet { a: face(0, 2).into(), b: face(0, 1).into() }];
|
||||
assert!(reversed.check(&a, &[&a]).is_err(), "meeting sides out of order");
|
||||
reversed.edges[0].origins = vec![EdgeOrigin::meet(face(0, 2), Side::new([face(0, 1), face(0, 6)]).unwrap())];
|
||||
assert!(reversed.check(&a, &[&a]).is_err(), "a side's origin beyond the operand's faces");
|
||||
reversed.edges[0].origins = vec![EdgeOrigin::meet(face(0, 2), Side::new([face(0, 1), face(0, 5)]).unwrap())];
|
||||
reversed.check(&a, &[&a]).unwrap();
|
||||
h.faces[0].origins = vec![face(0, 6)];
|
||||
assert!(h.check(&a, &[&a]).is_err(), "origin beyond the operand's faces");
|
||||
assert!(a.clone().with_history(h, &[&a]).is_err());
|
||||
@@ -98,7 +105,7 @@ fn substitute_composes_kept_split_merged_and_generated_faces() {
|
||||
],
|
||||
edges: vec![
|
||||
EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Edge { operand: 0, edge: 7 }] },
|
||||
EdgeProv { faces: vec![3, 4], origins: vec![EdgeOrigin::Meet { a: face(1, 4), b: FaceOrigin::Profile { segment: 2 } }] },
|
||||
EdgeProv { faces: vec![3, 4], origins: vec![EdgeOrigin::meet(face(1, 4), FaceOrigin::Profile { segment: 2 })] },
|
||||
],
|
||||
};
|
||||
let mut op2 = History {
|
||||
@@ -113,7 +120,7 @@ fn substitute_composes_kept_split_merged_and_generated_faces() {
|
||||
],
|
||||
edges: vec![
|
||||
EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Edge { operand: 1, edge: 1 }] },
|
||||
EdgeProv { faces: vec![2, 4], origins: vec![EdgeOrigin::Meet { a: face(1, 3), b: face(2, 1) }] },
|
||||
EdgeProv { faces: vec![2, 4], origins: vec![EdgeOrigin::meet(face(1, 3), face(2, 1))] },
|
||||
EdgeProv { faces: vec![4, 5], origins: vec![EdgeOrigin::Edge { operand: 2, edge: 3 }] },
|
||||
],
|
||||
};
|
||||
@@ -128,8 +135,8 @@ fn substitute_composes_kept_split_merged_and_generated_faces() {
|
||||
assert_eq!(f[4], prov(vec![face(0, 5), face(3, 1)], Change::Merged));
|
||||
assert_eq!(f[5], prov(vec![FaceOrigin::Edge { operand: 1, edge: 7 }], Change::Generated));
|
||||
let e = &op2.edges;
|
||||
assert_eq!(e[0].origins, vec![EdgeOrigin::Meet { a: face(2, 4), b: FaceOrigin::Profile { segment: 2 } }]);
|
||||
assert_eq!(e[1].origins, vec![EdgeOrigin::Meet { a: face(2, 4), b: face(3, 1) }]);
|
||||
assert_eq!(e[0].origins, vec![EdgeOrigin::meet(face(2, 4), FaceOrigin::Profile { segment: 2 })]);
|
||||
assert_eq!(e[1].origins, vec![EdgeOrigin::meet(face(2, 4), face(3, 1))]);
|
||||
assert_eq!(e[2].origins, vec![EdgeOrigin::Edge { operand: 3, edge: 3 }]);
|
||||
}
|
||||
|
||||
@@ -148,12 +155,75 @@ fn substitute_refuses_lineage_it_cannot_express_and_leaves_history_unchanged() {
|
||||
// A blend of an edge the inner operation generated.
|
||||
let mut blend = History { operands: 1, faces: vec![prov(vec![FaceOrigin::Edge { operand: 0, edge: 0 }], Change::Generated)], edges: vec![] };
|
||||
assert!(blend.substitute(0, &inner).is_err());
|
||||
// A meeting with a face the inner operation made from nothing.
|
||||
let bare = History { operands: 0, faces: vec![prov(vec![], Change::Generated)], edges: vec![] };
|
||||
let mut meet = History {
|
||||
operands: 1,
|
||||
faces: vec![prov(vec![face(0, 0)], Change::Modified), prov(vec![FaceOrigin::Cap { end: true }], Change::Generated)],
|
||||
edges: vec![EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::meet(face(0, 0), FaceOrigin::Cap { end: true })] }],
|
||||
};
|
||||
let before = meet.clone();
|
||||
assert!(meet.substitute(0, &bare).is_err());
|
||||
assert_eq!(meet, before);
|
||||
// Out-of-range references and operands.
|
||||
let mut bad = History { operands: 1, faces: vec![prov(vec![face(0, 9)], Change::Kept)], edges: vec![] };
|
||||
assert!(bad.substitute(0, &inner).is_err());
|
||||
assert!(bad.substitute(3, &inner).is_err());
|
||||
}
|
||||
|
||||
/// A side is a set: sorted, without repeats, never empty; a meeting puts the lesser side first.
|
||||
#[test]
|
||||
fn sides_and_meetings_are_canonical() {
|
||||
let side = |o: &[FaceOrigin]| Side::new(o.iter().copied()).unwrap();
|
||||
assert_eq!(side(&[face(1, 2), face(0, 5), face(1, 2)]).origins(), [face(0, 5), face(1, 2)]);
|
||||
assert!(Side::new([]).is_none());
|
||||
assert_eq!(Side::from(face(0, 1)).single(), Some(face(0, 1)));
|
||||
assert_eq!(side(&[face(0, 1), face(0, 2)]).single(), None);
|
||||
let (x, y) = (side(&[face(1, 0)]), side(&[face(0, 3), face(0, 4)]));
|
||||
assert_eq!(EdgeOrigin::meet(x.clone(), y.clone()), EdgeOrigin::meet(y.clone(), x.clone()));
|
||||
assert_eq!(EdgeOrigin::meet(x.clone(), y.clone()), EdgeOrigin::Meet { a: y, b: x });
|
||||
assert_eq!(EdgeOrigin::meet(face(2, 0), face(1, 0)), EdgeOrigin::meet(face(1, 0), face(2, 0)));
|
||||
}
|
||||
|
||||
/// An edge meeting a face the inner operation merged: that side becomes the merged face's
|
||||
/// origins, and the other side stays one face.
|
||||
#[test]
|
||||
fn substitute_composes_a_meeting_with_a_merged_face() {
|
||||
// Heal-like: the inner operation joined faces 1 and 2 of its operand into its face 0.
|
||||
let inner = History {
|
||||
operands: 1,
|
||||
faces: vec![prov(vec![face(0, 1), face(0, 2)], Change::Merged), prov(vec![face(0, 0)], Change::Kept)],
|
||||
edges: vec![EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Edge { operand: 0, edge: 4 }] }],
|
||||
};
|
||||
// Blend-like: a face in place of the inner edge 0, meeting both inner faces.
|
||||
let mut outer = History {
|
||||
operands: 1,
|
||||
faces: vec![
|
||||
prov(vec![face(0, 0)], Change::Modified),
|
||||
prov(vec![face(0, 1)], Change::Modified),
|
||||
prov(vec![FaceOrigin::Edge { operand: 0, edge: 0 }], Change::Generated),
|
||||
],
|
||||
edges: vec![
|
||||
EdgeProv { faces: vec![0, 2], origins: vec![EdgeOrigin::meet(face(0, 0), FaceOrigin::Edge { operand: 0, edge: 0 })] },
|
||||
EdgeProv { faces: vec![1, 2], origins: vec![EdgeOrigin::meet(face(0, 1), FaceOrigin::Edge { operand: 0, edge: 0 })] },
|
||||
],
|
||||
};
|
||||
outer.substitute(0, &inner).unwrap();
|
||||
let blend = Side::from(FaceOrigin::Edge { operand: 0, edge: 4 });
|
||||
let merged = Side::new([face(0, 2), face(0, 1)]).unwrap();
|
||||
assert_eq!(outer.faces[0], prov(vec![face(0, 1), face(0, 2)], Change::Merged));
|
||||
assert_eq!(outer.edges[0].origins, [EdgeOrigin::meet(blend.clone(), merged)]);
|
||||
assert_eq!(outer.edges[1].origins, [EdgeOrigin::meet(blend, face(0, 0))]);
|
||||
// Renumbering joins operands within a side, and keeps the meeting canonical.
|
||||
let mut two = History {
|
||||
operands: 2,
|
||||
faces: vec![],
|
||||
edges: vec![EdgeProv { faces: vec![], origins: vec![EdgeOrigin::meet(Side::new([face(0, 3), face(1, 3)]).unwrap(), face(1, 0))] }],
|
||||
};
|
||||
two.renumber(&[Some(1), Some(1)], 2).unwrap();
|
||||
assert_eq!(two.edges[0].origins, [EdgeOrigin::meet(face(1, 0), face(1, 3))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_sources_follow_kept_split_and_agreeing_merged_faces() {
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(4.0, 3.0, 1.0)).unwrap().with_box_certificate(7).unwrap();
|
||||
|
||||
Reference in new issue
Block a user