use crate::mass_properties::solid_signed_volume;
use crate::offset_shell::{flip_all_faces, orient_open_solid_faces};
use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, ShellRecord, VertexRecord};
use crate::{build_pcurve_on_surface, project_point_to_curve, NurbsCurve, Vec3};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SewReport {
pub edges_sewn: usize,
pub shells_merged: usize,
pub open_edges_before: usize,
pub open_edges_after: usize,
pub oriented_outward: bool,
pub issues: Vec<String>,
}
fn one_use_edge_ids(solid: &BrepSolid) -> HashSet<u64> {
let mut counts = HashMap::<u64, usize>::default();
for coedge in solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
{
*counts.entry(coedge.edge_id).or_default() += 1;
}
counts
.into_iter()
.filter(|(_, count)| *count == 1)
.map(|(id, _)| id)
.collect()
}
fn curve_point(edge: &EdgeRecord, fraction: f64) -> Result<Vec3, String> {
edge.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
}
fn locus_deviation(
piece: &EdgeRecord,
carrier: &EdgeRecord,
samples: usize,
) -> Result<f64, String> {
let mut worst = 0.0f64;
for index in 0..=samples {
let point = curve_point(piece, index as f64 / samples as f64)?;
worst = worst.max(project_point_to_curve(&carrier.curve, point)?.distance);
}
Ok(worst)
}
fn same_parametrization(
first: &EdgeRecord,
second: &EdgeRecord,
tolerance: f64,
) -> Result<bool, String> {
for index in 0..=8 {
let fraction = index as f64 / 8.0;
if curve_point(first, fraction)?
.sub(curve_point(second, fraction)?)
.length()
> tolerance
{
return Ok(false);
}
}
Ok(true)
}
fn walk_points(face: &FaceRecord, coedge: &CoedgeRecord) -> Result<[Vec3; 2], String> {
let [start, end] = coedge.pcurve.domain()?;
let sample = |fraction: f64| -> Result<Vec3, String> {
let uv = coedge.pcurve.evaluate(start + (end - start) * fraction)?;
face.surface.evaluate(uv.x, uv.y)
};
Ok([sample(0.0)?, sample(0.35)?])
}
struct RebindPatch {
shell: usize,
face: usize,
loop_index: usize,
coedge: usize,
forward: bool,
pcurve: Option<NurbsCurve>,
}
fn plan_rebind(
solid: &BrepSolid,
keep: &EdgeRecord,
remove: &EdgeRecord,
tolerance: f64,
) -> Result<Vec<RebindPatch>, String> {
let identical = same_parametrization(keep, remove, tolerance)?;
let closed = keep.start_vertex_id == keep.end_vertex_id;
let period = keep.t1 - keep.t0;
let mut patches = Vec::new();
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
for (loop_index, loop_record) in face.loops.iter().enumerate() {
for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
if coedge.edge_id != remove.id {
continue;
}
let [walk_start, walk_next] = walk_points(face, coedge)?;
let t_start = project_point_to_curve(&keep.curve, walk_start)?.u;
let t_next = project_point_to_curve(&keep.curve, walk_next)?.u;
let forward = if closed {
let mut delta = t_next - t_start;
while delta > period * 0.5 {
delta -= period;
}
while delta < -period * 0.5 {
delta += period;
}
delta > 0.0
} else {
t_next > t_start
};
let pcurve = if identical {
None
} else {
let traversal = if forward {
keep.curve.clone()
} else {
keep.curve.reversed()?
};
Some(build_pcurve_on_surface(&face.surface, &traversal)?)
};
patches.push(RebindPatch {
shell: shell_index,
face: face_index,
loop_index,
coedge: coedge_index,
forward,
pcurve,
});
}
}
}
}
Ok(patches)
}
fn apply_rebind(
solid: &mut BrepSolid,
keep: &EdgeRecord,
remove: &EdgeRecord,
patches: Vec<RebindPatch>,
tolerance: f64,
) {
for patch in patches {
let coedge = &mut solid.shells[patch.shell].faces[patch.face].loops[patch.loop_index]
.coedges[patch.coedge];
coedge.edge_id = keep.id;
coedge.forward = patch.forward;
if let Some(pcurve) = patch.pcurve {
coedge.pcurve = pcurve;
}
}
solid.edges.retain(|edge| edge.id != remove.id);
let point_of = |vertex_id: u64| {
solid
.vertices
.iter()
.find(|vertex| vertex.id == vertex_id)
.map(|vertex| vertex.point)
};
let mut welds = Vec::<(u64, u64)>::new();
for from in [remove.start_vertex_id, remove.end_vertex_id] {
let Some(from_point) = point_of(from) else {
continue;
};
let target = [keep.start_vertex_id, keep.end_vertex_id]
.into_iter()
.filter_map(|candidate| {
point_of(candidate).map(|point| (candidate, point.sub(from_point).length()))
})
.min_by(|first, second| first.1.total_cmp(&second.1));
if let Some((to, distance)) = target {
if distance <= tolerance && from != to {
welds.push((from, to));
}
}
}
for (from, to) in welds {
for edge in &mut solid.edges {
if edge.start_vertex_id == from {
edge.start_vertex_id = to;
}
if edge.end_vertex_id == from {
edge.end_vertex_id = to;
}
}
}
}
fn orient_pair<'a>(
solid: &BrepSolid,
first: &'a EdgeRecord,
second: &'a EdgeRecord,
points: &HashMap<u64, Vec3>,
tolerance: f64,
) -> Option<(&'a EdgeRecord, &'a EdgeRecord)> {
if first.start_vertex_id != first.end_vertex_id {
return Some((first, second));
}
let (Some(&first_seam), Some(&second_seam)) = (
points.get(&first.start_vertex_id),
points.get(&second.start_vertex_id),
) else {
return Some((first, second));
};
if first_seam.sub(second_seam).length() <= tolerance {
return Some((first, second));
}
let seam_is_exclusive = |rim: &EdgeRecord| {
!solid.edges.iter().any(|edge| {
edge.id != rim.id
&& (edge.start_vertex_id == rim.start_vertex_id
|| edge.end_vertex_id == rim.start_vertex_id)
})
};
if seam_is_exclusive(second) {
Some((first, second))
} else if seam_is_exclusive(first) {
Some((second, first))
} else {
None
}
}
fn merge_connected_shells(solid: &mut BrepSolid) -> usize {
let mut owner_of_edge = HashMap::<u64, usize>::default();
let mut parent = (0..solid.shells.len()).collect::<Vec<_>>();
fn root(parent: &mut [usize], index: usize) -> usize {
if parent[index] != index {
parent[index] = root(parent, parent[index]);
}
parent[index]
}
for (shell_index, shell) in solid.shells.iter().enumerate() {
for coedge in shell
.faces
.iter()
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
{
if let Some(&other) = owner_of_edge.get(&coedge.edge_id) {
let first = root(&mut parent, other);
let second = root(&mut parent, shell_index);
if first != second {
parent[second] = first;
}
} else {
owner_of_edge.insert(coedge.edge_id, shell_index);
}
}
}
let before = solid.shells.len();
let original = std::mem::take(&mut solid.shells);
let mut merged = Vec::<ShellRecord>::new();
let mut group_of = HashMap::<usize, usize>::default();
for (index, shell) in original.into_iter().enumerate() {
let group = root(&mut parent, index);
if let Some(&target) = group_of.get(&group) {
merged[target].faces.extend(shell.faces);
} else {
group_of.insert(group, merged.len());
merged.push(shell);
}
}
solid.shells = merged;
before - solid.shells.len()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum PairSearch {
Hashed,
#[cfg_attr(not(test), allow(dead_code))]
Exhaustive,
}
fn grid_cell(point: Vec3, cell: f64) -> Option<[i64; 3]> {
let mut key = [0i64; 3];
for (slot, value) in key.iter_mut().zip([point.x, point.y, point.z]) {
if !value.is_finite() {
return None;
}
let scaled = (value / cell).floor();
if !scaled.is_finite() || scaled.abs() > 9.0e15 {
return None;
}
*slot = scaled as i64;
}
Some(key)
}
struct EndpointGrid {
cell: f64,
buckets: HashMap<[i64; 3], Vec<usize>>,
}
impl EndpointGrid {
fn build(endpoints: &[Option<[Vec3; 2]>], closed: &[bool], tolerance: f64) -> Option<Self> {
let cell = 2.0 * tolerance;
if !(cell.is_finite() && cell > 0.0) {
return None;
}
let mut buckets = HashMap::<[i64; 3], Vec<usize>>::default();
for (position, ends) in endpoints.iter().enumerate() {
if closed[position] {
continue;
}
let Some(ends) = ends else {
continue;
};
for point in ends {
buckets
.entry(grid_cell(*point, cell)?)
.or_default()
.push(position);
}
}
Some(EndpointGrid { cell, buckets })
}
fn near(&self, point: Vec3, out: &mut Vec<usize>) -> bool {
out.clear();
let Some(centre) = grid_cell(point, self.cell) else {
return false;
};
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if let Some(bucket) =
self.buckets
.get(&[centre[0] + dx, centre[1] + dy, centre[2] + dz])
{
out.extend_from_slice(bucket);
}
}
}
}
out.sort_unstable();
out.dedup();
true
}
}
#[allow(clippy::too_many_arguments)]
fn offer_seconds(
grid: Option<&EndpointGrid>,
first_position: usize,
first_closed: bool,
endpoints: &[Option<[Vec3; 2]>],
closed_positions: &[usize],
candidate_count: usize,
neighbours: &mut Vec<usize>,
offered: &mut Vec<usize>,
) {
offered.clear();
let Some(grid) = grid else {
offered.extend(first_position + 1..candidate_count);
return;
};
if first_closed {
offered.extend(
closed_positions
.iter()
.copied()
.filter(|&position| position > first_position),
);
return;
}
let Some([start, _]) = endpoints[first_position] else {
return;
};
if grid.near(start, neighbours) {
offered.extend(
neighbours
.iter()
.copied()
.filter(|&position| position > first_position),
);
}
}
pub fn sew_solid(solid: &BrepSolid, tolerance: f64) -> Result<(BrepSolid, SewReport), String> {
let (sewn, report, _examined) = sew_solid_with_search(solid, tolerance, PairSearch::Hashed)?;
Ok((sewn, report))
}
fn sew_solid_with_search(
solid: &BrepSolid,
tolerance: f64,
search: PairSearch,
) -> Result<(BrepSolid, SewReport, usize), String> {
if !(tolerance.is_finite() && tolerance > 0.0) {
return Err("sew_solid: tolerance must be positive".into());
}
let mut result = solid.clone();
let open_edges_before = one_use_edge_ids(&result).len();
let mut edges_sewn = 0usize;
let mut pairs_examined = 0usize;
let mut blocked = HashSet::<(u64, u64)>::default();
let unordered = |first: u64, second: u64| (first.min(second), first.max(second));
let mut offered = Vec::<usize>::new();
let mut neighbours = Vec::<usize>::new();
loop {
let one_use = one_use_edge_ids(&result);
let candidates = result
.edges
.iter()
.enumerate()
.filter(|(_, edge)| !edge.degenerate && one_use.contains(&edge.id))
.map(|(index, _)| index)
.collect::<Vec<_>>();
let points = result
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect::<HashMap<_, _>>();
let closed = candidates
.iter()
.map(|&index| {
let edge = &result.edges[index];
edge.start_vertex_id == edge.end_vertex_id
})
.collect::<Vec<_>>();
let endpoints = candidates
.iter()
.map(|&index| {
let edge = &result.edges[index];
match (
points.get(&edge.start_vertex_id),
points.get(&edge.end_vertex_id),
) {
(Some(&start), Some(&end)) => Some([start, end]),
_ => None,
}
})
.collect::<Vec<_>>();
let closed_positions = (0..candidates.len())
.filter(|&position| closed[position])
.collect::<Vec<_>>();
let grid = match search {
PairSearch::Hashed => EndpointGrid::build(&endpoints, &closed, tolerance),
PairSearch::Exhaustive => None,
};
let mut chosen = None;
'pairs: for first_position in 0..candidates.len() {
let first = &result.edges[candidates[first_position]];
let first_closed = closed[first_position];
offer_seconds(
grid.as_ref(),
first_position,
first_closed,
&endpoints,
&closed_positions,
candidates.len(),
&mut neighbours,
&mut offered,
);
for second_position in offered.iter().copied() {
let second = &result.edges[candidates[second_position]];
pairs_examined += 1;
if blocked.contains(&unordered(first.id, second.id)) {
continue;
}
if first_closed != closed[second_position] {
continue;
}
if !first_closed {
let (Some([fs, fe]), Some([ss, se])) =
(endpoints[first_position], endpoints[second_position])
else {
continue;
};
let direct =
fs.sub(ss).length() <= tolerance && fe.sub(se).length() <= tolerance;
let reversed =
fs.sub(se).length() <= tolerance && fe.sub(ss).length() <= tolerance;
if !direct && !reversed {
continue;
}
}
if locus_deviation(first, second, 8)? > tolerance
|| locus_deviation(second, first, 8)? > tolerance
{
continue;
}
let Some((keep, remove)) = orient_pair(&result, first, second, &points, tolerance)
else {
continue;
};
chosen = Some((keep.clone(), remove.clone()));
break 'pairs;
}
}
let Some((keep, remove)) = chosen else {
break;
};
match plan_rebind(&result, &keep, &remove, tolerance) {
Ok(patches) => {
apply_rebind(&mut result, &keep, &remove, patches, tolerance);
edges_sewn += 1;
}
Err(_) => {
blocked.insert(unordered(keep.id, remove.id));
}
}
}
let used_vertices = result
.edges
.iter()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect::<HashSet<_>>();
result
.vertices
.retain(|vertex| used_vertices.contains(&vertex.id));
let shells_merged = merge_connected_shells(&mut result);
let mut oriented_outward = false;
if edges_sewn > 0 {
orient_open_solid_faces(&mut result)?;
if let Ok(volume) = solid_signed_volume(&result) {
if volume < 0.0 {
flip_all_faces(&mut result)?;
}
oriented_outward = volume.abs() > tolerance * tolerance * tolerance;
}
}
if edges_sewn > 0 || shells_merged > 0 {
let vertex_count = result.vertices.len() as i64;
let edge_count = result.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
let face_count = result
.shells
.iter()
.map(|shell| shell.faces.len())
.sum::<usize>() as i64;
let ring_count = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| face.loops.len().saturating_sub(1))
.sum::<usize>() as i64;
let numerator = 2 - (vertex_count - edge_count + face_count - ring_count);
if numerator >= 0 && numerator % 2 == 0 {
result.genus = numerator / 2;
}
}
let open_edges_after = one_use_edge_ids(&result).len();
let issues = result
.validate()
.into_iter()
.map(|issue| issue.message)
.collect();
Ok((
result,
SewReport {
edges_sewn,
shells_merged,
open_edges_before,
open_edges_after,
oriented_outward,
issues,
},
pairs_examined,
))
}
pub fn split_pinched_vertices(solid: &mut BrepSolid) -> Result<usize, String> {
let mut split_count = 0usize;
let vertex_ids: Vec<u64> = solid.vertices.iter().map(|vertex| vertex.id).collect();
let mut next_id = solid
.vertices
.iter()
.map(|vertex| vertex.id)
.chain(solid.edges.iter().map(|edge| edge.id))
.max()
.unwrap_or(0)
+ 1;
let edge_of_id: HashMap<u64, usize> = solid
.edges
.iter()
.enumerate()
.map(|(index, edge)| (edge.id, index))
.collect();
let vertex_of_id: HashMap<u64, usize> = solid
.vertices
.iter()
.enumerate()
.map(|(index, vertex)| (vertex.id, index))
.collect();
for vertex_id in vertex_ids {
let incident: Vec<u64> = solid
.edges
.iter()
.filter(|edge| edge.start_vertex_id == vertex_id || edge.end_vertex_id == vertex_id)
.map(|edge| edge.id)
.collect();
if incident.len() < 4 {
continue;
}
let index_of: HashMap<u64, usize> = incident
.iter()
.enumerate()
.map(|(index, id)| (*id, index))
.collect();
let mut parent: Vec<usize> = (0..incident.len()).collect();
fn root(parent: &mut [usize], index: usize) -> usize {
if parent[index] != index {
parent[index] = root(parent, parent[index]);
}
parent[index]
}
let edge_end = |edge_id: u64, forward: bool| -> Option<u64> {
edge_of_id.get(&edge_id).map(|&index| {
let edge = &solid.edges[index];
if forward {
edge.end_vertex_id
} else {
edge.start_vertex_id
}
})
};
for shell in &solid.shells {
for face in &shell.faces {
for loop_record in &face.loops {
let count = loop_record.coedges.len();
for index in 0..count {
let current = &loop_record.coedges[index];
let next = &loop_record.coedges[(index + 1) % count];
let Some(junction) = edge_end(current.edge_id, current.forward) else {
continue;
};
if junction != vertex_id {
continue;
}
let (Some(&a), Some(&b)) =
(index_of.get(¤t.edge_id), index_of.get(&next.edge_id))
else {
continue;
};
let ra = root(&mut parent, a);
let rb = root(&mut parent, b);
if ra != rb {
parent[rb] = ra;
}
}
}
}
}
let mut component_of: HashMap<usize, usize> = HashMap::default();
let mut components = 0usize;
let mut assignment: Vec<usize> = vec![0; incident.len()];
for index in 0..incident.len() {
let r = root(&mut parent, index);
let component = *component_of.entry(r).or_insert_with(|| {
components += 1;
components - 1
});
assignment[index] = component;
}
if components < 2 {
continue;
}
let point = vertex_of_id
.get(&vertex_id)
.map(|&index| solid.vertices[index].point)
.ok_or("split_pinched_vertices: vertex vanished")?;
let mut replacement_ids = vec![vertex_id];
for _ in 1..components {
let id = next_id;
next_id += 1;
solid.vertices.push(VertexRecord { id, point });
replacement_ids.push(id);
}
for (offset, edge_id) in incident.iter().enumerate() {
let replacement = replacement_ids[assignment[offset]];
if replacement == vertex_id {
continue;
}
if let Some(edge) = solid.edges.iter_mut().find(|edge| edge.id == *edge_id) {
if edge.start_vertex_id == vertex_id {
edge.start_vertex_id = replacement;
}
if edge.end_vertex_id == vertex_id {
edge.end_vertex_id = replacement;
}
}
}
split_count += components - 1;
}
Ok(split_count)
}