Merge pull request #1326 from hannobraun/validate

Move cycle validation to new infrastructure
This commit is contained in:
Hanno Braun 2022-11-08 14:52:12 +01:00 committed by GitHub
commit 00a290e7ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 221 additions and 98 deletions

10
Cargo.lock generated
View File

@ -1187,6 +1187,7 @@ dependencies = [
"anyhow", "anyhow",
"fj-interop", "fj-interop",
"fj-math", "fj-math",
"itertools",
"parking_lot", "parking_lot",
"pretty_assertions", "pretty_assertions",
"robust-predicates", "robust-predicates",
@ -1836,6 +1837,15 @@ version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.4" version = "1.0.4"

View File

@ -13,6 +13,7 @@ categories.workspace = true
[dependencies] [dependencies]
fj-interop.workspace = true fj-interop.workspace = true
fj-math.workspace = true fj-math.workspace = true
itertools = "0.10.5"
parking_lot = "0.12.0" parking_lot = "0.12.0"
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
robust-predicates = "0.1.4" robust-predicates = "0.1.4"

View File

@ -55,34 +55,32 @@ impl CycleBuilder for PartialCycle {
.surface() .surface()
.expect("Need surface to extend cycle with poly-chain"); .expect("Need surface to extend cycle with poly-chain");
let position_prev = vertex_prev let [position_prev, position_next] =
.position() [&vertex_prev, &vertex_next].map(|vertex| {
.expect("Need surface position to extend cycle"); vertex
let position_next = vertex_next .position()
.position() .expect("Need surface position to extend cycle")
.expect("Need surface position to extend cycle"); });
let from = vertex_prev; previous = Some(vertex_next.clone());
let to = vertex_next;
previous = Some(to.clone());
let curve = Curve::partial() let curve = Curve::partial()
.with_surface(Some(surface.clone())) .with_surface(Some(surface.clone()))
.update_as_line_from_points([position_prev, position_next]); .update_as_line_from_points([position_prev, position_next]);
let [from, to] = let vertices = [(0., vertex_prev), (1., vertex_next)].map(
[(0., from), (1., to)].map(|(position, surface_form)| { |(position, surface_form)| {
Vertex::partial() Vertex::partial()
.with_curve(curve.clone()) .with_curve(curve.clone())
.with_position(Some([position])) .with_position(Some([position]))
.with_surface_form(surface_form) .with_surface_form(surface_form)
}); },
);
half_edges.push( half_edges.push(
HalfEdge::partial() HalfEdge::partial()
.with_curve(curve) .with_curve(curve)
.with_vertices([from, to]), .with_vertices(vertices),
); );
continue; continue;

View File

@ -6,7 +6,7 @@ use crate::{
Curve, GlobalVertex, Objects, Surface, SurfaceVertex, Vertex, Curve, GlobalVertex, Objects, Surface, SurfaceVertex, Vertex,
VerticesInNormalizedOrder, VerticesInNormalizedOrder,
}, },
partial::{HasPartial, PartialGlobalEdge, PartialHalfEdge}, partial::{HasPartial, MaybePartial, PartialGlobalEdge, PartialHalfEdge},
storage::Handle, storage::Handle,
validate::ValidationError, validate::ValidationError,
}; };
@ -15,6 +15,12 @@ use super::{CurveBuilder, GlobalVertexBuilder};
/// Builder API for [`PartialHalfEdge`] /// Builder API for [`PartialHalfEdge`]
pub trait HalfEdgeBuilder: Sized { pub trait HalfEdgeBuilder: Sized {
/// Update the partial half-edge with the given back vertex
fn with_back_vertex(self, back: impl Into<MaybePartial<Vertex>>) -> Self;
/// Update the partial half-edge with the given front vertex
fn with_front_vertex(self, front: impl Into<MaybePartial<Vertex>>) -> Self;
/// Update partial half-edge as a circle, from the given radius /// Update partial half-edge as a circle, from the given radius
/// ///
/// # Implementation Note /// # Implementation Note
@ -37,9 +43,22 @@ pub trait HalfEdgeBuilder: Sized {
/// Update partial half-edge as a line segment, reusing existing vertices /// Update partial half-edge as a line segment, reusing existing vertices
fn update_as_line_segment(self) -> Self; fn update_as_line_segment(self) -> Self;
/// Infer the global form of the partial half-edge
fn infer_global_form(self) -> Self;
} }
impl HalfEdgeBuilder for PartialHalfEdge { impl HalfEdgeBuilder for PartialHalfEdge {
fn with_back_vertex(self, back: impl Into<MaybePartial<Vertex>>) -> Self {
let [_, front] = self.vertices();
self.with_vertices([back.into(), front])
}
fn with_front_vertex(self, front: impl Into<MaybePartial<Vertex>>) -> Self {
let [back, _] = self.vertices();
self.with_vertices([back, front.into()])
}
fn update_as_circle_from_radius( fn update_as_circle_from_radius(
self, self,
radius: impl Into<Scalar>, radius: impl Into<Scalar>,
@ -181,6 +200,10 @@ impl HalfEdgeBuilder for PartialHalfEdge {
self.with_curve(curve).with_vertices([back, front]) self.with_curve(curve).with_vertices([back, front])
} }
fn infer_global_form(self) -> Self {
self.with_global_form(PartialGlobalEdge::default())
}
} }
/// Builder API for [`PartialGlobalEdge`] /// Builder API for [`PartialGlobalEdge`]

View File

@ -1,6 +1,7 @@
use std::slice;
use fj_interop::ext::SliceExt; use fj_interop::ext::SliceExt;
use fj_math::{Scalar, Winding}; use fj_math::{Scalar, Winding};
use pretty_assertions::assert_eq;
use crate::{path::SurfacePath, storage::Handle}; use crate::{path::SurfacePath, storage::Handle};
@ -24,48 +25,14 @@ impl Cycle {
pub fn new(half_edges: impl IntoIterator<Item = Handle<HalfEdge>>) -> Self { pub fn new(half_edges: impl IntoIterator<Item = Handle<HalfEdge>>) -> Self {
let half_edges = half_edges.into_iter().collect::<Vec<_>>(); let half_edges = half_edges.into_iter().collect::<Vec<_>>();
let surface = match half_edges.first() { // This is not a validation check, and thus not part of the validation
Some(half_edge) => half_edge.surface().clone(), // infrastructure. The property being checked here is inherent to the
None => panic!("Cycle must contain at least one half-edge"), // validity of a `Cycle`, as methods of `Cycle` might assume that there
}; // is at least one edge.
assert!(
// Verify, that the curves of all edges are defined in the correct !half_edges.is_empty(),
// surface. "Cycle must contain at least one half-edge"
for edge in &half_edges { );
assert_eq!(
surface.id(),
edge.curve().surface().id(),
"Edges in cycle not defined in same surface"
);
}
if half_edges.len() != 1 {
// Verify that all edges connect.
for [a, b] in half_edges.as_slice().array_windows_ext() {
let [_, prev] = a.vertices();
let [next, _] = b.vertices();
assert_eq!(
prev.surface_form().id(),
next.surface_form().id(),
"Edges in cycle do not connect"
);
}
}
// Verify that the edges form a cycle
if let Some(first) = half_edges.first() {
if let Some(last) = half_edges.last() {
let [first, _] = first.vertices();
let [_, last] = last.vertices();
assert_eq!(
first.surface_form().id(),
last.surface_form().id(),
"Edges do not form a cycle"
);
}
}
Self { half_edges } Self { half_edges }
} }
@ -82,7 +49,7 @@ impl Cycle {
} }
/// Access the half-edges that make up the cycle /// Access the half-edges that make up the cycle
pub fn half_edges(&self) -> impl Iterator<Item = &Handle<HalfEdge>> + '_ { pub fn half_edges(&self) -> HalfEdgesOfCycle {
self.half_edges.iter() self.half_edges.iter()
} }
@ -144,3 +111,8 @@ impl Cycle {
unreachable!("Encountered invalid cycle: {self:#?}"); unreachable!("Encountered invalid cycle: {self:#?}");
} }
} }
/// An iterator over the half-edges of a [`Cycle`]
///
/// Returned by [`Cycle::half_edges`].
pub type HalfEdgesOfCycle<'a> = slice::Iter<'a, Handle<HalfEdge>>;

View File

@ -85,7 +85,7 @@ mod vertex;
pub use self::{ pub use self::{
curve::{Curve, GlobalCurve}, curve::{Curve, GlobalCurve},
cycle::Cycle, cycle::{Cycle, HalfEdgesOfCycle},
edge::{GlobalEdge, HalfEdge, VerticesInNormalizedOrder}, edge::{GlobalEdge, HalfEdge, VerticesInNormalizedOrder},
face::{Face, FaceSet, Handedness}, face::{Face, FaceSet, Handedness},
shell::Shell, shell::Shell,
@ -103,8 +103,8 @@ use crate::{
path::GlobalPath, path::GlobalPath,
storage::{Handle, Store}, storage::{Handle, Store},
validate::{ validate::{
HalfEdgeValidationError, SurfaceVertexValidationError, Validate2, CycleValidationError, HalfEdgeValidationError,
VertexValidationError, SurfaceVertexValidationError, Validate2, VertexValidationError,
}, },
}; };
@ -187,7 +187,10 @@ pub struct Cycles {
impl Cycles { impl Cycles {
/// Insert a [`Cycle`] into the store /// Insert a [`Cycle`] into the store
pub fn insert(&self, cycle: Cycle) -> Result<Handle<Cycle>, Infallible> { pub fn insert(
&self,
cycle: Cycle,
) -> Result<Handle<Cycle>, CycleValidationError> {
cycle.validate()?; cycle.validate()?;
Ok(self.store.insert(cycle)) Ok(self.store.insert(cycle))
} }

View File

@ -67,8 +67,14 @@ impl<T: HasPartial> MaybePartial<T> {
/// Merge this `MaybePartial` with another of the same type /// Merge this `MaybePartial` with another of the same type
pub fn merge_with(self, other: impl Into<Self>) -> Self { pub fn merge_with(self, other: impl Into<Self>) -> Self {
match (self, other.into()) { match (self, other.into()) {
(Self::Full(_), Self::Full(_)) => { (Self::Full(a), Self::Full(b)) => {
panic!("Can't merge two full objects") if a.id() != b.id() {
panic!("Can't merge two full objects")
}
// If they're equal, which they are, if we reach this point,
// then merging them is a no-op.
Self::Full(a)
} }
(Self::Full(full), Self::Partial(_)) (Self::Full(full), Self::Partial(_))
| (Self::Partial(_), Self::Full(full)) => Self::Full(full), | (Self::Partial(_), Self::Full(full)) => Self::Full(full),
@ -218,6 +224,14 @@ impl MaybePartial<SurfaceVertex> {
Self::Partial(partial) => partial.surface(), Self::Partial(partial) => partial.surface(),
} }
} }
/// Access the global form
pub fn global_form(&self) -> MaybePartial<GlobalVertex> {
match self {
Self::Full(full) => full.global_form().clone().into(),
Self::Partial(partial) => partial.global_form(),
}
}
} }
impl MaybePartial<Vertex> { impl MaybePartial<Vertex> {

View File

@ -1,4 +1,5 @@
use crate::{ use crate::{
builder::HalfEdgeBuilder,
objects::{Cycle, HalfEdge, Objects, Surface}, objects::{Cycle, HalfEdge, Objects, Surface},
partial::{ partial::{
util::merge_options, MaybePartial, PartialHalfEdge, PartialVertex, util::merge_options, MaybePartial, PartialHalfEdge, PartialVertex,
@ -86,6 +87,22 @@ impl PartialCycle {
mut self, mut self,
objects: &Objects, objects: &Objects,
) -> Result<Handle<Cycle>, ValidationError> { ) -> Result<Handle<Cycle>, ValidationError> {
// Check that the cycle is closed. This will lead to a panic further
// down anyway, but that panic would be super-confusing. This one should
// be a bit more explicit on what is wrong.
if let (Some(first), Some(last)) =
(self.half_edges.first(), self.half_edges.last())
{
let [first, _] = first.vertices();
let [_, last] = last.vertices();
assert_eq!(
first.surface_form().position(),
last.surface_form().position(),
"Attempting to build un-closed cycle"
);
}
// To create a cycle, we need to make sure that all its half-edges // To create a cycle, we need to make sure that all its half-edges
// connect to each other. Let's start with all the connections between // connect to each other. Let's start with all the connections between
// the first and the last half-edge. // the first and the last half-edge.
@ -96,14 +113,11 @@ impl PartialCycle {
half_edge.front().surface_form().into_full(objects)?; half_edge.front().surface_form().into_full(objects)?;
*half_edge = half_edge.clone().merge_with( *half_edge = half_edge.clone().merge_with(
PartialHalfEdge::default() PartialHalfEdge::default().with_vertices([
.with_back_vertex( PartialVertex::default().with_surface_form(back_vertex),
PartialVertex::default().with_surface_form(back_vertex), PartialVertex::default()
) .with_surface_form(front_vertex.clone()),
.with_front_vertex( ]),
PartialVertex::default()
.with_surface_form(front_vertex.clone()),
),
); );
previous_vertex = Some(MaybePartial::from(front_vertex)); previous_vertex = Some(MaybePartial::from(front_vertex));

View File

@ -73,28 +73,6 @@ impl PartialHalfEdge {
self self
} }
/// Update the partial half-edge with the given back vertex
pub fn with_back_vertex(
mut self,
vertex: impl Into<MaybePartial<Vertex>>,
) -> Self {
let [from, _] = &mut self.vertices;
*from = vertex.into();
self
}
/// Update the partial half-edge with the given front vertex
pub fn with_front_vertex(
mut self,
vertex: impl Into<MaybePartial<Vertex>>,
) -> Self {
let [_, to] = &mut self.vertices;
*to = vertex.into();
self
}
/// Update the partial half-edge with the given vertices /// Update the partial half-edge with the given vertices
pub fn with_vertices( pub fn with_vertices(
mut self, mut self,

View File

@ -1,16 +1,116 @@
use std::convert::Infallible; use itertools::Itertools;
use crate::objects::Cycle; use crate::{
objects::{Cycle, SurfaceVertex},
storage::Handle,
};
use super::{Validate2, ValidationConfig}; use super::{Validate2, ValidationConfig};
impl Validate2 for Cycle { impl Validate2 for Cycle {
type Error = Infallible; type Error = CycleValidationError;
fn validate_with_config( fn validate_with_config(
&self, &self,
_: &ValidationConfig, _: &ValidationConfig,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
CycleValidationError::check_half_edge_connections(self)?;
// We don't need to check that all half-edges are defined in the same
// surface. We already check that they are connected by identical
// surface vertices, so that would be redundant.
Ok(())
}
}
/// [`Cycle`] validation error
#[derive(Debug, thiserror::Error)]
pub enum CycleValidationError {
/// Half-edges are not connected
#[error(
"`HalfEdge`s of `Cycle` are not connected\n\
- Front vertex of previous `HalfEdge`: {prev:#?}\n\
- Back vertex of next `HalfEdge`: {next:#?}"
)]
HalfEdgeConnection {
/// The front vertex of the previous half-edge
prev: Handle<SurfaceVertex>,
/// The back vertex of the next half-edge
next: Handle<SurfaceVertex>,
},
}
impl CycleValidationError {
fn check_half_edge_connections(cycle: &Cycle) -> Result<(), Self> {
for (a, b) in cycle.half_edges().circular_tuple_windows() {
let [_, prev] = a.vertices();
let [next, _] = b.vertices();
let prev = prev.surface_form();
let next = next.surface_form();
if prev.id() != next.id() {
return Err(Self::HalfEdgeConnection {
prev: prev.clone(),
next: next.clone(),
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{
builder::{CycleBuilder, HalfEdgeBuilder, VertexBuilder},
objects::{Cycle, Objects},
partial::HasPartial,
validate::Validate2,
};
#[test]
fn cycle_half_edge_connections() -> anyhow::Result<()> {
let objects = Objects::new();
let valid = Cycle::partial()
.with_poly_chain_from_points(
objects.surfaces.xy_plane(),
[[0., 0.], [1., 0.], [0., 1.]],
)
.close_with_line_segment()
.build(&objects)?;
let invalid = {
let mut half_edges = valid
.half_edges()
.map(|half_edge| half_edge.to_partial())
.collect::<Vec<_>>();
let first_half_edge = &mut half_edges[0];
let [first_vertex, _] = first_half_edge.vertices();
// Sever connection between the last and first half-edge in the
// cycle.
let first_vertex = first_vertex.into_partial().infer_surface_form();
*first_half_edge = first_half_edge
.clone()
.with_back_vertex(first_vertex)
.infer_global_form();
let half_edges = half_edges
.into_iter()
.map(|half_edge| half_edge.build(&objects))
.collect::<Result<Vec<_>, _>>()?;
Cycle::new(half_edges)
};
assert!(valid.validate().is_ok());
assert!(invalid.validate().is_err());
Ok(()) Ok(())
} }
} }

View File

@ -24,6 +24,11 @@ impl Validate2 for HalfEdge {
HalfEdgeValidationError::check_global_curve_identity(self)?; HalfEdgeValidationError::check_global_curve_identity(self)?;
HalfEdgeValidationError::check_global_vertex_identity(self)?; HalfEdgeValidationError::check_global_vertex_identity(self)?;
HalfEdgeValidationError::check_vertex_positions(self, config)?; HalfEdgeValidationError::check_vertex_positions(self, config)?;
// We don't need to check anything about surfaces here. We already check
// curves, which makes sure the vertices are consistent with each other,
// and the validation of those vertices checks the surfaces.
Ok(()) Ok(())
} }
} }

View File

@ -26,6 +26,7 @@ mod uniqueness;
mod vertex; mod vertex;
pub use self::{ pub use self::{
cycle::CycleValidationError,
edge::HalfEdgeValidationError, edge::HalfEdgeValidationError,
uniqueness::UniquenessIssues, uniqueness::UniquenessIssues,
vertex::{SurfaceVertexValidationError, VertexValidationError}, vertex::{SurfaceVertexValidationError, VertexValidationError},
@ -177,6 +178,10 @@ pub enum ValidationError {
#[error("Uniqueness validation failed")] #[error("Uniqueness validation failed")]
Uniqueness(#[from] UniquenessIssues), Uniqueness(#[from] UniquenessIssues),
/// `Cycle` validation error
#[error(transparent)]
Cycle(#[from] CycleValidationError),
/// `HalfEdge` validation error /// `HalfEdge` validation error
#[error(transparent)] #[error(transparent)]
HalfEdge(#[from] HalfEdgeValidationError), HalfEdge(#[from] HalfEdgeValidationError),