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
@@ -7,7 +7,7 @@ use std::collections::BTreeSet;
|
||||
use solvecraft_geom::{Mesh, Vec3};
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::history::{Change, EdgeOrigin, FaceOrigin, History};
|
||||
use crate::history::{Change, EdgeOrigin, FaceOrigin, History, Side};
|
||||
use crate::{BoolOp, boolean, box_solid};
|
||||
|
||||
const fn face(operand: usize, face: usize) -> FaceOrigin {
|
||||
@@ -98,9 +98,9 @@ pub(crate) fn checked<'a>(r: &'a Body, operands: &[&Body], tol: f64, at: Reach,
|
||||
FaceOrigin::Offset { operand, face } => to_face(&ms[operand], face, p) < at.made + near,
|
||||
_ => false,
|
||||
};
|
||||
let along = |o: &EdgeOrigin, p: &Vec3| match *o {
|
||||
EdgeOrigin::Edge { operand, edge } => to_edge(&ms[operand], edge, *p) < near.max(at.moved),
|
||||
EdgeOrigin::Meet { a, b } => on(&a, *p) && on(&b, *p),
|
||||
let along = |o: &EdgeOrigin, p: &Vec3| match o {
|
||||
EdgeOrigin::Edge { operand, edge } => to_edge(&ms[*operand], *edge, *p) < near.max(at.moved),
|
||||
EdgeOrigin::Meet { a, b } => a.origins().iter().any(|o| on(o, *p)) && b.origins().iter().any(|o| on(o, *p)),
|
||||
};
|
||||
for (i, f) in h.faces.iter().enumerate() {
|
||||
assert!(!f.origins.is_empty() || generated(i), "face {i} has no origin");
|
||||
@@ -116,8 +116,12 @@ pub(crate) fn checked<'a>(r: &'a Body, operands: &[&Body], tol: f64, at: Reach,
|
||||
let pts = &rm.edges[j];
|
||||
for o in &e.origins {
|
||||
match o {
|
||||
// A joined edge runs along all of its origins, each along part of it.
|
||||
EdgeOrigin::Edge { .. } if e.origins.len() > 1 => assert!(pts.iter().any(|p| along(o, p)), "edge {j}: {o:?} is not along it"),
|
||||
// A joined edge runs along all of its origins, each along part of it (one in the
|
||||
// middle of a straight edge holds none of its points, but its own lie on it).
|
||||
EdgeOrigin::Edge { operand, edge } if e.origins.len() > 1 => assert!(
|
||||
pts.iter().any(|p| along(o, p)) || ms[*operand].edges[*edge].iter().any(|q| to_edge(&rm, j, *q) < near.max(at.moved)),
|
||||
"edge {j}: {o:?} is not along it"
|
||||
),
|
||||
_ => assert!(pts.iter().all(|p| along(o, p)), "edge {j} ({e:?}) does not lie along {o:?}"),
|
||||
}
|
||||
}
|
||||
@@ -160,9 +164,10 @@ fn from(h: &History, o: FaceOrigin) -> Vec<usize> {
|
||||
(0..h.faces.len()).filter(|i| h.faces[*i].origins.contains(&o)).collect()
|
||||
}
|
||||
|
||||
/// The meetings among the edges' origins, as face pairs.
|
||||
/// The meetings among the edges' origins, as face pairs (each side one face here).
|
||||
fn meets(h: &History) -> BTreeSet<(FaceOrigin, FaceOrigin)> {
|
||||
h.edges.iter().flat_map(|e| &e.origins).filter_map(|o| if let EdgeOrigin::Meet { a, b } = o { Some((*a, *b)) } else { None }).collect()
|
||||
let one = |s: &Side| s.single().expect("a side of one face");
|
||||
h.edges.iter().flat_map(|e| &e.origins).filter_map(|o| if let EdgeOrigin::Meet { a, b } = o { Some((one(a), one(b))) } else { None }).collect()
|
||||
}
|
||||
|
||||
fn boxed(x0: f64, y0: f64, z0: f64, x1: f64, y1: f64, z1: f64) -> Body {
|
||||
@@ -419,3 +424,44 @@ fn faceted_tool() {
|
||||
assert_eq!(from_a.len(), 6);
|
||||
assert_eq!(h.faces.iter().filter(|f| matches!(f.origins.first(), Some(FaceOrigin::Face { operand: 1, .. }))).count(), 3);
|
||||
}
|
||||
|
||||
/// A step cut into a certified box: the boolean leaves the faces it split on the box's planes
|
||||
/// in pieces, which the planar regularization joins back; the joined body keeps the boolean's
|
||||
/// history composed through the join.
|
||||
#[test]
|
||||
fn regularized_step_cut() {
|
||||
let (pa, pb) = (boxed(0.0, 0.0, 0.0, 30.0, 35.0, 7.5), boxed(0.0, 30.0, 2.5, 22.5, 50.0, 25.0));
|
||||
let (a, b) = (pa.clone().with_box_certificate(1).unwrap(), pb.clone().with_box_certificate(2).unwrap());
|
||||
let r = boolean(&a, &b, BoolOp::Cut).unwrap().unwrap();
|
||||
let h = verified(&r, &[&a, &b], 0.01);
|
||||
let plain = boolean(&pa, &pb, BoolOp::Cut).unwrap().unwrap();
|
||||
let ph = verified(&plain, &[&pa, &pb], 0.01);
|
||||
// Uncertified, the box's face 3 stays in two pieces; certified, they are joined into one
|
||||
// face, the only one from it.
|
||||
assert_eq!(from(ph, face(0, 3)).len(), 2, "{}", show(ph));
|
||||
assert_eq!(r.face_count() + 1, plain.face_count());
|
||||
let joined = from(h, face(0, 3));
|
||||
assert_eq!(joined.len(), 1, "{}", show(h));
|
||||
assert_eq!(faces_of(h)[joined[0]], (Change::Modified, vec![face(0, 3)]));
|
||||
// Every other face is as the boolean made it, in its order.
|
||||
let rest = |h: &History| -> Vec<(Change, Vec<FaceOrigin>)> { faces_of(h).into_iter().filter(|(_, o)| *o != [face(0, 3)]).collect() };
|
||||
assert_eq!(rest(h), rest(ph));
|
||||
}
|
||||
|
||||
/// A ring joined under a floor, its bore on the floor's hole (coincident cylinders): the ring's
|
||||
/// bore is narrowed, the bodies joined, and the bore cut out again. The ring's bore is a circle
|
||||
/// built in two arcs, and the recut wall is split where the cutting cylinder's own construction
|
||||
/// splits it, so which arc each piece of it restores is not known: no history.
|
||||
#[test]
|
||||
fn bore_union_of_a_bore_in_two_arcs_has_no_history() {
|
||||
let hole = crate::cylinder(Vec3::new(50.0, 40.0, -5.0), Vec3::Z, 10.0, 30.0).unwrap();
|
||||
let floor = boolean(&boxed(0.0, 0.0, 0.0, 100.0, 80.0, 10.0), &hole, BoolOp::Cut).unwrap().unwrap();
|
||||
let centre = solvecraft_geom::Vec2::new(50.0, 40.0);
|
||||
let annulus = solvecraft_geom::Region2 {
|
||||
outer: solvecraft_geom::Loop2::circle(centre, 20.0),
|
||||
holes: vec![solvecraft_geom::Loop2::circle(centre, 10.0).reversed()],
|
||||
};
|
||||
let ring = crate::extrude(&solvecraft_geom::Plane::XY, &[annulus], -5.0, 2.0).unwrap().remove(0);
|
||||
let r = crate::coplanar::bore_union(&floor, &ring, &|_| true).expect("the bores are recut");
|
||||
assert!(r.history().is_none());
|
||||
}
|
||||
@@ -112,7 +112,7 @@ pub(crate) fn shapeops(h: &truck_shapeops::History, tool: &Solid, tool_edges: Op
|
||||
tool_edges.and_then(|t| t.get(passed.get(edge)?)).cloned()
|
||||
}
|
||||
truck_shapeops::EdgeOrigin::Edge { operand, edge } => Some(vec![EdgeOrigin::Edge { operand, edge }]),
|
||||
truck_shapeops::EdgeOrigin::Meet { a, b } => Some(vec![EdgeOrigin::Meet { a: face(a), b: face(b) }]),
|
||||
truck_shapeops::EdgeOrigin::Meet { a, b } => Some(vec![EdgeOrigin::meet(face(a), face(b))]),
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some((faces, edges))
|
||||
|
||||
@@ -357,14 +357,15 @@ fn trimmed(x: &Body, h: History, slab: &Body, operands: [&Body; 2], n: Vec3, d:
|
||||
f.origins = origins.into_iter().collect();
|
||||
}
|
||||
}
|
||||
let cap = crate::history::Side::from(cap);
|
||||
for e in &mut cut.edges {
|
||||
for o in &mut e.origins {
|
||||
let side = match *o {
|
||||
EdgeOrigin::Meet { a, b } if a == cap => b,
|
||||
EdgeOrigin::Meet { a, b } if b == cap => a,
|
||||
let side = match o {
|
||||
EdgeOrigin::Meet { a, b } if *a == cap => b,
|
||||
EdgeOrigin::Meet { a, b } if *b == cap => a,
|
||||
_ => continue,
|
||||
};
|
||||
let FaceOrigin::Face { operand, face } = side else { return None };
|
||||
let Some(FaceOrigin::Face { operand, face }) = side.single() else { return None };
|
||||
let (plane_faces, bounds) = (in_plane.get(operand)?, bounds.get(operand)?);
|
||||
let mut along = (0..bounds.len())
|
||||
.filter(|k| bounds.get(*k).is_some_and(|fs| fs.len() == 2 && fs.contains(&face) && fs.iter().any(|f| plane_faces.contains(f))));
|
||||
@@ -541,8 +542,12 @@ fn same_cylinder(x: &CylFace, y: &CylFace, tol: f64) -> bool {
|
||||
/// The history over `[a, b]` of `r`, made in [`bore_union`]: `joined` (the target joined with
|
||||
/// `narrowed`, the tool with its bore narrowed) less `cut`, the cylinder whose wall restores the
|
||||
/// tool's bore faces `bore` (the tool is operand `tool_operand`). Nothing of the cylinder but its
|
||||
/// wall may be left.
|
||||
/// wall may be left. Only a bore of one face: the cylinder's wall faces are split where its own
|
||||
/// construction splits them, so which of several bore faces a piece of it lies over is not known.
|
||||
fn recut(r: &Body, joined: &Body, narrowed: &Body, cut: &Body, bore: &[usize], tool_operand: usize) -> Option<History> {
|
||||
if bore.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
// Over [target, tool].
|
||||
let joined = crate::boolean_record::through(joined, 1, narrowed.history())?;
|
||||
let wall = || record::region(bore.iter().map(|&face| FaceOrigin::Face { operand: 1, face }).collect(), false);
|
||||
|
||||
@@ -446,7 +446,7 @@ impl HealMap {
|
||||
.map(|(id, under)| {
|
||||
let mut origins = BTreeSet::new();
|
||||
for i in under {
|
||||
origins.extend(edges.get(*i)?.iter().copied());
|
||||
origins.extend(edges.get(*i)?.iter().cloned());
|
||||
}
|
||||
Some((*id, origins.into_iter().collect()))
|
||||
})
|
||||
|
||||
@@ -48,13 +48,64 @@ pub struct FaceProv {
|
||||
pub change: Change,
|
||||
}
|
||||
|
||||
/// One side of a meeting: the origins of the face on that side of the edge. One origin, or
|
||||
/// several where that face was made from several (a merged face) and the edge lies along it
|
||||
/// as a whole. Never empty; sorted, without repeats, so equal sides compare equal.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Side(Vec<FaceOrigin>);
|
||||
|
||||
impl Side {
|
||||
/// The side made of `origins` (in any order, repeats allowed); `None` when there are none.
|
||||
pub fn new(origins: impl IntoIterator<Item = FaceOrigin>) -> Option<Side> {
|
||||
let side = Side::canonical(origins.into_iter().collect());
|
||||
(!side.0.is_empty()).then_some(side)
|
||||
}
|
||||
|
||||
/// Its origins, sorted.
|
||||
pub fn origins(&self) -> &[FaceOrigin] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// The side's one origin, when it has exactly one.
|
||||
pub fn single(&self) -> Option<FaceOrigin> {
|
||||
match self.0.as_slice() {
|
||||
[o] => Some(*o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same side with each origin mapped by `f` (failing where `f` does).
|
||||
fn try_map(&self, f: impl Fn(FaceOrigin) -> Result<FaceOrigin>) -> Result<Side> {
|
||||
Ok(Side::canonical(self.0.iter().map(|o| f(*o)).collect::<Result<_>>()?))
|
||||
}
|
||||
|
||||
/// The same side with each origin's operand renumbered by `f`.
|
||||
fn map_operand(&self, f: impl Fn(usize) -> usize) -> Side {
|
||||
Side::canonical(self.0.iter().map(|o| o.map_operand(&f)).collect())
|
||||
}
|
||||
|
||||
/// `origins` sorted, without repeats.
|
||||
fn canonical(mut origins: Vec<FaceOrigin>) -> Side {
|
||||
origins.sort_unstable();
|
||||
origins.dedup();
|
||||
Side(origins)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FaceOrigin> for Side {
|
||||
fn from(origin: FaceOrigin) -> Side {
|
||||
Side(vec![origin])
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a result edge came from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum EdgeOrigin {
|
||||
/// (Part of) edge `edge` of input `operand`.
|
||||
Edge { operand: usize, edge: usize },
|
||||
/// Where two faces meet that met nowhere before (a boolean's intersection curve).
|
||||
Meet { a: FaceOrigin, b: FaceOrigin },
|
||||
/// Where two faces meet that met nowhere before (a boolean's intersection curve): the
|
||||
/// origins of the face on each side, `a <= b` (made with [`EdgeOrigin::meet`]).
|
||||
Meet { a: Side, b: Side },
|
||||
}
|
||||
|
||||
/// A result edge: the result faces it separates and what it came from (empty when it is new
|
||||
@@ -91,11 +142,18 @@ impl FaceOrigin {
|
||||
}
|
||||
|
||||
impl EdgeOrigin {
|
||||
/// Where the faces with origins `a` and `b` (sides, or single origins) meet, in canonical
|
||||
/// order (the lesser side first).
|
||||
pub fn meet(a: impl Into<Side>, b: impl Into<Side>) -> EdgeOrigin {
|
||||
let (a, b) = (a.into(), b.into());
|
||||
if a <= b { EdgeOrigin::Meet { a, b } } else { EdgeOrigin::Meet { a: b, b: a } }
|
||||
}
|
||||
|
||||
/// The same origin with its operands renumbered by `f`.
|
||||
fn map_operand(self, f: impl Fn(usize) -> usize) -> EdgeOrigin {
|
||||
fn map_operand(&self, f: impl Fn(usize) -> usize) -> EdgeOrigin {
|
||||
match self {
|
||||
EdgeOrigin::Edge { operand, edge } => EdgeOrigin::Edge { operand: f(operand), edge },
|
||||
EdgeOrigin::Meet { a, b } => EdgeOrigin::Meet { a: a.map_operand(&f), b: b.map_operand(&f) },
|
||||
EdgeOrigin::Edge { operand, edge } => EdgeOrigin::Edge { operand: f(*operand), edge: *edge },
|
||||
EdgeOrigin::Meet { a, b } => EdgeOrigin::meet(a.map_operand(&f), b.map_operand(&f)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,20 +201,24 @@ impl History {
|
||||
}
|
||||
let ok = e.origins.iter().all(|o| match o {
|
||||
EdgeOrigin::Edge { operand, edge } => counts.get(*operand).is_some_and(|c| *edge < c.1),
|
||||
EdgeOrigin::Meet { a, b } => face_ok(a) && face_ok(b),
|
||||
EdgeOrigin::Meet { a, b } => a.origins().iter().chain(b.origins()).all(face_ok),
|
||||
});
|
||||
if !ok {
|
||||
return Err(failed("an edge origin lies outside its operand"));
|
||||
}
|
||||
if e.origins.iter().any(|o| matches!(o, EdgeOrigin::Meet { a, b } if a > b)) {
|
||||
return Err(failed("a meeting's sides are out of order"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose with the history of the operation that made operand `k`: origins on operand `k`
|
||||
/// become that operation's origins, its operands (none, for a body made from a sketch)
|
||||
/// taking `k`'s place in the operand list. Fails, without changing `self`, where a lineage
|
||||
/// cannot be expressed (an offset of a face that operation generated, a blend of an edge
|
||||
/// it generated, an edge between merged faces).
|
||||
/// taking `k`'s place in the operand list; a meeting's side becomes the origins of the faces
|
||||
/// it names (several where that operation merged faces). Fails, without changing `self`,
|
||||
/// where a lineage cannot be expressed (an offset of a face that operation generated or
|
||||
/// merged, a blend of an edge it generated, a meeting with a face it made from nothing).
|
||||
pub fn substitute(&mut self, k: usize, inner: &History) -> Result<()> {
|
||||
if k >= self.operands {
|
||||
return Err(failed("no such operand"));
|
||||
@@ -209,26 +271,30 @@ impl History {
|
||||
};
|
||||
faces.push(FaceProv { origins, change });
|
||||
}
|
||||
// One side of a meeting: a single face of the composed operation.
|
||||
let side = |o: FaceOrigin| -> Result<FaceOrigin> {
|
||||
match map_face(o)?.0.as_slice() {
|
||||
[x] => Ok(*x),
|
||||
_ => Err(failed("an edge meets a face the inner operation merged")),
|
||||
// Split where an origin lies under several composed faces (pieces the inner operation
|
||||
// split and this one joined back, or dropped all but one of, are not).
|
||||
crate::record::mark_shared_split(&mut faces);
|
||||
// One side of a meeting: the origins of what its faces were made from.
|
||||
let side = |s: &Side| -> Result<Side> {
|
||||
let mut origins = Vec::new();
|
||||
for o in s.origins() {
|
||||
origins.extend(map_face(*o)?.0);
|
||||
}
|
||||
Side::new(origins).ok_or_else(|| failed("an edge meets a face the inner operation made from nothing"))
|
||||
};
|
||||
let mut edges = Vec::with_capacity(self.edges.len());
|
||||
for e in &self.edges {
|
||||
let mut origins = BTreeSet::new();
|
||||
for o in &e.origins {
|
||||
match *o {
|
||||
EdgeOrigin::Edge { operand, edge } if operand == k => {
|
||||
origins.extend(inner_edge(edge)?.origins.iter().map(|x| x.map_operand(lifted)))
|
||||
match o {
|
||||
EdgeOrigin::Edge { operand, edge } if *operand == k => {
|
||||
origins.extend(inner_edge(*edge)?.origins.iter().map(|x| x.map_operand(lifted)))
|
||||
}
|
||||
EdgeOrigin::Edge { .. } => {
|
||||
origins.insert(o.map_operand(outer));
|
||||
}
|
||||
EdgeOrigin::Meet { a, b } => {
|
||||
origins.insert(EdgeOrigin::Meet { a: side(a)?, b: side(b)? });
|
||||
origins.insert(EdgeOrigin::meet(side(a)?, side(b)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,9 +334,9 @@ impl History {
|
||||
let origins: BTreeSet<EdgeOrigin> = e
|
||||
.origins
|
||||
.iter()
|
||||
.map(|o| match *o {
|
||||
EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge { operand: to(operand)?, edge }),
|
||||
EdgeOrigin::Meet { a, b } => Ok(EdgeOrigin::Meet { a: face(a)?, b: face(b)? }),
|
||||
.map(|o| match o {
|
||||
EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge { operand: to(*operand)?, edge: *edge }),
|
||||
EdgeOrigin::Meet { a, b } => Ok(EdgeOrigin::meet(a.try_map(face)?, b.try_map(face)?)),
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
edges.push(EdgeProv { faces: e.faces.clone(), origins: origins.into_iter().collect() });
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -193,7 +193,7 @@ fn chamfer_cutters_mitre_at_a_corner() {
|
||||
}
|
||||
// The two chamfers meet along the mitre.
|
||||
let (a, c) = (edge(edge_at(&b, pts[0])), edge(edge_at(&b, pts[1])));
|
||||
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::Meet { a, b: c }] || e.origins == [EdgeOrigin::Meet { a: c, b: a }]));
|
||||
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::meet(a, c)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -218,7 +218,7 @@ fn delete_a_fillet() {
|
||||
// The faces either side of the round meet again where they met nowhere in its input.
|
||||
let (top, back_face) = (face_at(&r, Vec3::new(20.0, 10.0, 20.0)), face_at(&r, Vec3::new(20.0, 30.0, 5.0)));
|
||||
let (x, y) = (face(top), face(back_face));
|
||||
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::Meet { a: x.min(y), b: x.max(y) }]));
|
||||
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::meet(x, y)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -376,3 +376,60 @@ fn mirrored_copy_keeps_every_face_and_edge() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The block with box cuts of each common shape: a notch through the back, a pocket, a step
|
||||
/// along one end, a slot across the top, a hole through, a corner bitten out.
|
||||
fn box_cuts() -> Vec<(&'static str, Body, Body)> {
|
||||
let b = cube();
|
||||
[
|
||||
("notch", Vec3::new(15.0, 25.0, -5.0), Vec3::new(25.0, 35.0, 25.0)),
|
||||
("pocket", Vec3::new(10.0, 10.0, 10.0), Vec3::new(30.0, 20.0, 25.0)),
|
||||
("step", Vec3::new(30.0, -5.0, 10.0), Vec3::new(45.0, 35.0, 25.0)),
|
||||
("slot", Vec3::new(15.0, -5.0, 10.0), Vec3::new(25.0, 35.0, 25.0)),
|
||||
("hole", Vec3::new(15.0, 10.0, -5.0), Vec3::new(25.0, 20.0, 25.0)),
|
||||
("bite", Vec3::new(30.0, 20.0, 10.0), Vec3::new(45.0, 35.0, 25.0)),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(name, lo, hi)| {
|
||||
let tool = box_solid(lo, hi).unwrap();
|
||||
let cut = boolean(&b, &tool, BoolOp::Cut).unwrap().unwrap();
|
||||
(name, cut, tool)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Meetings with a side of several origins (a face the modifier's heal merged).
|
||||
fn merged_meetings(h: &History) -> usize {
|
||||
h.edges.iter().flat_map(|e| &e.origins).filter(|o| matches!(o, EdgeOrigin::Meet { a, b } if a.single().is_none() || b.single().is_none())).count()
|
||||
}
|
||||
|
||||
/// A fillet right after a box cut. Where the cut leaves coplanar pieces (the step, slot and
|
||||
/// bite leave the front face in pieces), the fillet's heal merges them, and the edge where the
|
||||
/// round runs into that face meets all of it. Composed with the cut, over the block and the tool.
|
||||
#[test]
|
||||
fn fillet_after_box_cuts() {
|
||||
let b = cube();
|
||||
let at = Vec3::new(5.0, 0.0, 20.0);
|
||||
for (name, cut, tool) in box_cuts() {
|
||||
let r = crate::fillet(&cut, &[at], 2.0).unwrap();
|
||||
let h = verified(&r, &cut, reach(2.0 * 1.5));
|
||||
assert_eq!(blends(h), [vec![edge(edge_at(&cut, at))]], "{name}");
|
||||
let merged = matches!(name, "step" | "slot" | "bite");
|
||||
assert_eq!(merged_meetings(h) > 0, merged, "{name}: {h:?}");
|
||||
let mut through = h.clone();
|
||||
through.substitute(0, cut.history().unwrap()).unwrap();
|
||||
through.check(&r, &[&b, &tool]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A chamfer right after a box cut whose cutter's cut goes the prism way: the cutter's walls meet
|
||||
/// the cut body's walls where a segment was split off.
|
||||
#[test]
|
||||
fn chamfer_after_box_cuts() {
|
||||
let at = Vec3::new(5.0, 0.0, 20.0);
|
||||
for (name, cut, _) in box_cuts().into_iter().filter(|(name, ..)| matches!(*name, "notch" | "hole")) {
|
||||
let r = crate::chamfer(&cut, &[at], 2.0).unwrap();
|
||||
let h = verified(&r, &cut, reach(2.0 * 1.5));
|
||||
assert_eq!(with(h, &[edge(edge_at(&cut, at))]).len(), 1, "{name}: {h:?}");
|
||||
}
|
||||
}
|
||||
@@ -587,7 +587,7 @@ impl Sources {
|
||||
fn edge(&self, p: FaceOrigin, q: FaceOrigin, ends: (Vec3, Vec3), tol: f64) -> Option<Vec<EdgeOrigin>> {
|
||||
let (FaceOrigin::Face { operand: x, face: f }, FaceOrigin::Face { operand: y, face: g }) = (p, q) else { return None };
|
||||
if x != y {
|
||||
return Some(vec![EdgeOrigin::Meet { a: p.min(q), b: p.max(q) }]);
|
||||
return Some(vec![EdgeOrigin::meet(p, q)]);
|
||||
}
|
||||
let Some(edges) = self.edges.get(x)?.as_ref() else { return Some(Vec::new()) };
|
||||
if f == g {
|
||||
|
||||
@@ -13,7 +13,7 @@ use truck_modeling as mt;
|
||||
use super::{Index, Section, Tag, dist_to, inside};
|
||||
use crate::BoolOp;
|
||||
use crate::body::{Body, edge_order, from_p3};
|
||||
use crate::history::{EdgeOrigin, FaceOrigin, FaceOrigin::Face, FaceProv};
|
||||
use crate::history::{EdgeOrigin, FaceOrigin, FaceOrigin::Face, FaceProv, Side};
|
||||
|
||||
/// A cap of an operand: a planar face square to the prism direction.
|
||||
struct Cap {
|
||||
@@ -188,7 +188,8 @@ impl Context<'_> {
|
||||
|
||||
/// The origins of the edge between the wall over a segment tagged `t` and a cap lying on the
|
||||
/// operand faces `cap`: the operand's edge between the wall and its own cap there (the very
|
||||
/// edge the segment was read off, when that is one), else where the wall meets the cap.
|
||||
/// edge the segment was read off, when that is one), else where the wall meets the cap (all of
|
||||
/// the faces it lies on).
|
||||
fn cap_edge(&self, t: Tag, cap: &BTreeSet<FaceOrigin>) -> Option<Vec<EdgeOrigin>> {
|
||||
let (x, w) = (t.operand, t.wall?);
|
||||
let own: Vec<usize> =
|
||||
@@ -199,33 +200,32 @@ impl Context<'_> {
|
||||
return Some(vec![EdgeOrigin::Edge { operand: x, edge: e }]);
|
||||
}
|
||||
let along: Vec<usize> = own.iter().flat_map(|c| self.edges_between(x, [w, *c], None)).collect();
|
||||
match (along.as_slice(), cap.first()) {
|
||||
([e], _) => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]),
|
||||
([], Some(o)) if cap.len() == 1 => {
|
||||
let wall = Self::wall(t)?;
|
||||
Some(vec![EdgeOrigin::Meet { a: wall.min(*o), b: wall.max(*o) }])
|
||||
}
|
||||
match along.as_slice() {
|
||||
[e] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]),
|
||||
[] => Some(vec![EdgeOrigin::meet(Self::wall(t)?, Side::new(cap.iter().copied())?)]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The origins of the edge between the walls over two segments where the first (tagged
|
||||
/// `t1`) runs into the second (`t2`): the operand's edge there between its walls (at the
|
||||
/// operand vertex the segments meet at), where two operands' walls meet, or none within one
|
||||
/// wall (a segment split there).
|
||||
/// operand vertex the segments meet at, else the one edge between them), where two
|
||||
/// operands' walls meet, or none within one wall (a segment split there).
|
||||
fn junction(&self, t1: Tag, t2: Tag) -> Option<Vec<EdgeOrigin>> {
|
||||
let (p, q) = (Self::wall(t1)?, Self::wall(t2)?);
|
||||
if t1.operand != t2.operand {
|
||||
return Some(vec![EdgeOrigin::Meet { a: p.min(q), b: p.max(q) }]);
|
||||
return Some(vec![EdgeOrigin::meet(p, q)]);
|
||||
}
|
||||
let (x, w1, w2) = (t1.operand, t1.wall?, t2.wall?);
|
||||
match (t1.end, t2.start) {
|
||||
(Some(v), Some(u)) if v == u => match self.edges_between(x, [w1, w2], Some(v)).as_slice() {
|
||||
[e] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]),
|
||||
_ => None,
|
||||
},
|
||||
// At that vertex: the operand's edge there (a seam, within one wall), else two edges
|
||||
// of one wall's cap outline running on through it.
|
||||
(Some(v), Some(u)) if v == u => self.one_edge(x, [w1, w2], Some(v)).or_else(|| (w1 == w2).then(Vec::new)),
|
||||
_ if w1 == w2 => Some(Vec::new()),
|
||||
_ => None,
|
||||
// Elsewhere (a segment split off there, or read off another cap): only the operand's
|
||||
// walls are around the edge, so the operand has its edge between them there, and
|
||||
// that is the one edge between them where there is one.
|
||||
_ => self.one_edge(x, [w1, w2], None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,10 +238,16 @@ impl Context<'_> {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
if t1.operand != t2.operand {
|
||||
return Some(vec![EdgeOrigin::Meet { a: p.min(q), b: p.max(q) }]);
|
||||
return Some(vec![EdgeOrigin::meet(p, q)]);
|
||||
}
|
||||
match self.edges_between(t1.operand, [t1.wall?, t2.wall?], None).as_slice() {
|
||||
[e] => Some(vec![EdgeOrigin::Edge { operand: t1.operand, edge: *e }]),
|
||||
self.one_edge(t1.operand, [t1.wall?, t2.wall?], None)
|
||||
}
|
||||
|
||||
/// The operand's edge between the faces `faces` (touching vertex `at` when given), when it
|
||||
/// has exactly one.
|
||||
fn one_edge(&self, operand: usize, faces: [usize; 2], at: Option<usize>) -> Option<Vec<EdgeOrigin>> {
|
||||
match self.edges_between(operand, faces, at).as_slice() {
|
||||
[edge] => Some(vec![EdgeOrigin::Edge { operand, edge: *edge }]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ pub(crate) fn attach(body: Body, operands: &[Operand], faces: Option<Vec<FacePro
|
||||
}
|
||||
}
|
||||
let (edges, bounds) = edge_order(body.solid.face_iter());
|
||||
let edges = edges.iter().zip(bounds).map(|(e, faces)| EdgeProv { faces, origins: owned.get(&e.id()).copied().into_iter().collect() }).collect();
|
||||
let edges = edges.iter().zip(bounds).map(|(e, faces)| EdgeProv { faces, origins: owned.get(&e.id()).cloned().into_iter().collect() }).collect();
|
||||
let history = History { operands: operands.len(), faces, edges };
|
||||
let counts: Vec<(usize, usize)> = operands.iter().map(|o| (o.body.face_count(), o.edges.len())).collect();
|
||||
body.with_history_counts(history, &counts)
|
||||
@@ -323,15 +323,16 @@ impl Rebuild {
|
||||
}
|
||||
|
||||
/// The origins of a new edge between the result faces `between` (whose provenance is in
|
||||
/// `faces`): the meeting of the two faces, when each comes from one origin and they differ;
|
||||
/// else none (a seam, an edge between pieces of one face).
|
||||
/// `faces`): the meeting of the two faces (each side all of its face's origins, one or
|
||||
/// several), when each comes from something and no origin is under both; else none (a seam,
|
||||
/// an edge between pieces of one face, a face new to the operation).
|
||||
pub(crate) fn meeting(between: &[usize], faces: &[FaceProv]) -> Vec<EdgeOrigin> {
|
||||
let side = |k: usize| match between.get(k).and_then(|f| faces.get(*f)).map(|p| p.origins.as_slice()) {
|
||||
Some([o]) => Some(*o),
|
||||
_ => None,
|
||||
};
|
||||
match (between.len(), side(0), side(1)) {
|
||||
(2, Some(a), Some(b)) if a != b => vec![EdgeOrigin::Meet { a: a.min(b), b: a.max(b) }],
|
||||
let side = |f: &usize| faces.get(*f).and_then(|p| crate::history::Side::new(p.origins.iter().copied()));
|
||||
match between {
|
||||
[f, g] => match (side(f), side(g)) {
|
||||
(Some(a), Some(b)) if a.origins().iter().all(|o| !b.origins().contains(o)) => vec![EdgeOrigin::meet(a, b)],
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use solvecraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::build::PathSeg;
|
||||
use crate::history::{Change, FaceOrigin, History};
|
||||
use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History, Side};
|
||||
|
||||
const P: fn(usize) -> FaceOrigin = |segment| FaceOrigin::Profile { segment };
|
||||
const START: FaceOrigin = FaceOrigin::Cap { end: false };
|
||||
@@ -550,3 +550,23 @@ fn emboss_reports_kept_trimmed_and_profile_faces() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A new edge's meeting: each side all of its face's origins (a merged face's several); none
|
||||
/// where an origin lies on both sides (pieces of one face) or a face comes from nothing.
|
||||
#[test]
|
||||
fn a_new_edge_meets_whole_faces() {
|
||||
let f = |face: usize| FaceOrigin::Face { operand: 0, face };
|
||||
let blend = FaceOrigin::Edge { operand: 0, edge: 3 };
|
||||
let faces = vec![
|
||||
FaceProv { origins: vec![f(0), f(1)], change: Change::Merged },
|
||||
FaceProv { origins: vec![blend], change: Change::Generated },
|
||||
FaceProv { origins: vec![f(1)], change: Change::Split },
|
||||
FaceProv { origins: vec![], change: Change::Generated },
|
||||
];
|
||||
let merged = Side::new([f(0), f(1)]).unwrap();
|
||||
assert_eq!(super::meeting(&[1, 0], &faces), [EdgeOrigin::meet(merged, blend)]);
|
||||
assert_eq!(super::meeting(&[2, 1], &faces), [EdgeOrigin::meet(f(1), blend)]);
|
||||
assert!(super::meeting(&[0, 2], &faces).is_empty());
|
||||
assert!(super::meeting(&[1, 3], &faces).is_empty());
|
||||
assert!(super::meeting(&[1], &faces).is_empty());
|
||||
}
|
||||
Reference in new issue
Block a user