Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move half-edge connection check from Cycle to Face/Sketch #2322

Merged
merged 5 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions crates/fj-core/src/validate/cycle.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
use crate::{
geometry::Geometry,
topology::Cycle,
validation::{
checks::AdjacentHalfEdgesNotConnected, ValidationCheck,
ValidationConfig, ValidationError,
},
validation::{ValidationConfig, ValidationError},
};

use super::Validate;

impl Validate for Cycle {
fn validate(
&self,
config: &ValidationConfig,
errors: &mut Vec<ValidationError>,
geometry: &Geometry,
_: &ValidationConfig,
_: &mut Vec<ValidationError>,
_: &Geometry,
) {
errors.extend(
AdjacentHalfEdgesNotConnected::check(self, geometry, config)
.map(Into::into),
);
}
}
9 changes: 8 additions & 1 deletion crates/fj-core/src/validate/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use crate::{
geometry::Geometry,
topology::Face,
validation::{
checks::{FaceHasNoBoundary, InteriorCycleHasInvalidWinding},
checks::{
AdjacentHalfEdgesNotConnected, FaceHasNoBoundary,
InteriorCycleHasInvalidWinding,
},
ValidationCheck, ValidationConfig, ValidationError,
},
};
Expand All @@ -16,6 +19,10 @@ impl Validate for Face {
errors: &mut Vec<ValidationError>,
geometry: &Geometry,
) {
errors.extend(
AdjacentHalfEdgesNotConnected::check(self, geometry, config)
.map(Into::into),
);
errors.extend(
FaceHasNoBoundary::check(self, geometry, config).map(Into::into),
);
Expand Down
15 changes: 12 additions & 3 deletions crates/fj-core/src/validate/sketch.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use crate::geometry::Geometry;
use crate::{storage::Handle, topology::Cycle};
use crate::{topology::Sketch, validate_references};
use fj_math::Winding;

use crate::{
geometry::Geometry,
storage::Handle,
topology::{Cycle, Sketch},
validate_references,
validation::{checks::AdjacentHalfEdgesNotConnected, ValidationCheck},
};

use super::{
references::{ReferenceCountError, ReferenceCounter},
Validate, ValidationConfig, ValidationError,
Expand All @@ -15,6 +20,10 @@ impl Validate for Sketch {
errors: &mut Vec<ValidationError>,
geometry: &Geometry,
) {
errors.extend(
AdjacentHalfEdgesNotConnected::check(self, geometry, config)
.map(Into::into),
);
SketchValidationError::check_object_references(self, config, errors);
SketchValidationError::check_exterior_cycles(
self, geometry, config, errors,
Expand Down
10 changes: 5 additions & 5 deletions crates/fj-core/src/validation/checks/face_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ use crate::{
pub struct FaceHasNoBoundary {}

impl ValidationCheck<Face> for FaceHasNoBoundary {
fn check(
object: &Face,
_: &Geometry,
_: &ValidationConfig,
) -> impl Iterator<Item = Self> {
fn check<'r>(
object: &'r Face,
_: &'r Geometry,
_: &'r ValidationConfig,
) -> impl Iterator<Item = Self> + 'r {
let error = if object.region().exterior().half_edges().is_empty() {
Some(FaceHasNoBoundary {})
} else {
Expand Down
10 changes: 5 additions & 5 deletions crates/fj-core/src/validation/checks/face_winding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ pub struct InteriorCycleHasInvalidWinding {
}

impl ValidationCheck<Face> for InteriorCycleHasInvalidWinding {
fn check(
object: &Face,
geometry: &Geometry,
_: &ValidationConfig,
) -> impl Iterator<Item = Self> {
fn check<'r>(
object: &'r Face,
geometry: &'r Geometry,
_: &'r ValidationConfig,
) -> impl Iterator<Item = Self> + 'r {
object.region().interiors().iter().filter_map(|interior| {
let exterior = object.region().exterior();

Expand Down
134 changes: 93 additions & 41 deletions crates/fj-core/src/validation/checks/half_edge_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use fj_math::{Point, Scalar};
use crate::{
geometry::Geometry,
storage::Handle,
topology::{Cycle, HalfEdge},
topology::{Cycle, Face, HalfEdge, Region, Sketch},
validation::{validation_check::ValidationCheck, ValidationConfig},
};

Expand Down Expand Up @@ -57,50 +57,82 @@ pub struct AdjacentHalfEdgesNotConnected {
pub unconnected_half_edges: [Handle<HalfEdge>; 2],
}

impl ValidationCheck<Cycle> for AdjacentHalfEdgesNotConnected {
fn check(
object: &Cycle,
geometry: &Geometry,
config: &ValidationConfig,
) -> impl Iterator<Item = Self> {
object.half_edges().pairs().filter_map(|(first, second)| {
let end_pos_of_first_half_edge = {
let [_, end] = geometry.of_half_edge(first).boundary.inner;
geometry
.of_half_edge(first)
.path
.point_from_path_coords(end)
};
let start_pos_of_second_half_edge =
geometry.of_half_edge(second).start_position();

let distance_between_positions = (end_pos_of_first_half_edge
- start_pos_of_second_half_edge)
.magnitude();

if distance_between_positions > config.identical_max_distance {
return Some(AdjacentHalfEdgesNotConnected {
end_pos_of_first_half_edge,
start_pos_of_second_half_edge,
distance_between_positions,
unconnected_half_edges: [first.clone(), second.clone()],
});
}

None
})
impl ValidationCheck<Face> for AdjacentHalfEdgesNotConnected {
fn check<'r>(
object: &'r Face,
geometry: &'r Geometry,
config: &'r ValidationConfig,
) -> impl Iterator<Item = Self> + 'r {
check_region(object.region(), geometry, config)
}
}

impl ValidationCheck<Sketch> for AdjacentHalfEdgesNotConnected {
fn check<'r>(
object: &'r Sketch,
geometry: &'r Geometry,
config: &'r ValidationConfig,
) -> impl Iterator<Item = Self> + 'r {
object
.regions()
.iter()
.flat_map(|region| check_region(region, geometry, config))
}
}

fn check_region<'r>(
region: &'r Region,
geometry: &'r Geometry,
config: &'r ValidationConfig,
) -> impl Iterator<Item = AdjacentHalfEdgesNotConnected> + 'r {
[region.exterior()]
.into_iter()
.chain(region.interiors())
.flat_map(|cycle| check_cycle(cycle, geometry, config))
}

fn check_cycle<'r>(
cycle: &'r Cycle,
geometry: &'r Geometry,
config: &'r ValidationConfig,
) -> impl Iterator<Item = AdjacentHalfEdgesNotConnected> + 'r {
cycle.half_edges().pairs().filter_map(|(first, second)| {
let end_pos_of_first_half_edge = {
let [_, end] = geometry.of_half_edge(first).boundary.inner;
geometry
.of_half_edge(first)
.path
.point_from_path_coords(end)
};
let start_pos_of_second_half_edge =
geometry.of_half_edge(second).start_position();

let distance_between_positions = (end_pos_of_first_half_edge
- start_pos_of_second_half_edge)
.magnitude();

if distance_between_positions > config.identical_max_distance {
return Some(AdjacentHalfEdgesNotConnected {
end_pos_of_first_half_edge,
start_pos_of_second_half_edge,
distance_between_positions,
unconnected_half_edges: [first.clone(), second.clone()],
});
}

None
})
}

#[cfg(test)]
mod tests {

use crate::{
operations::{
build::{BuildCycle, BuildHalfEdge},
update::UpdateCycle,
build::{BuildFace, BuildHalfEdge},
update::{UpdateCycle, UpdateFace, UpdateRegion},
},
topology::{Cycle, HalfEdge},
topology::{Face, HalfEdge},
validation::ValidationCheck,
Core,
};
Expand All @@ -111,16 +143,36 @@ mod tests {
fn adjacent_half_edges_not_connected() -> anyhow::Result<()> {
let mut core = Core::new();

let valid = Cycle::polygon([[0., 0.], [1., 0.], [1., 1.]], &mut core);
// We're only testing for `Face` here, not `Sketch`. Should be fine, as
// most of the code is shared.
let valid = Face::polygon(
core.layers.topology.surfaces.space_2d(),
[[0., 0.], [1., 0.], [1., 1.]],
&mut core,
);
AdjacentHalfEdgesNotConnected::check_and_return_first_error(
&valid,
&core.layers.geometry,
)?;

let invalid = valid.update_half_edge(
valid.half_edges().first(),
|_, core| {
[HalfEdge::line_segment([[0., 0.], [2., 0.]], None, core)]
let invalid = valid.update_region(
|region, core| {
region.update_exterior(
|cycle, core| {
cycle.update_half_edge(
cycle.half_edges().first(),
|_, core| {
[HalfEdge::line_segment(
[[0., 0.], [2., 0.]],
None,
core,
)]
},
core,
)
},
core,
)
},
&mut core,
);
Expand Down
10 changes: 5 additions & 5 deletions crates/fj-core/src/validation/validation_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use super::ValidationConfig;
/// to. `Self` is the object, while `T` identifies the validation check.
pub trait ValidationCheck<T>: Sized {
/// Run the validation check on the implementing object
fn check(
object: &T,
geometry: &Geometry,
config: &ValidationConfig,
) -> impl Iterator<Item = Self>;
fn check<'r>(
object: &'r T,
geometry: &'r Geometry,
config: &'r ValidationConfig,
) -> impl Iterator<Item = Self> + 'r;

/// Convenience method to run the check return the first error
///
Expand Down