use crate::{KernelRefusal, KernelStage, OrRefuse};
use super::*;
use crate::offset_carve::carve_folded_trim_scanned;
use crate::offset_regularity::{scan_offset_regularity, Certainty, ScanBudget, TrimRegion};
use crate::{NurbsCurve, NurbsSurface};
pub(super) fn standalone_face(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, KernelRefusal> {
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.cloned()
.ok_or_else(|| format!("offset_shell: missing face {face_id}")).or_refuse(KernelStage::Refine, "offset_shell_missing_face")?;
let edge_ids = face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
.collect::<HashSet<_>>();
let edges = solid
.edges
.iter()
.filter(|edge| edge_ids.contains(&edge.id))
.cloned()
.collect::<Vec<_>>();
let vertex_ids = edges
.iter()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect::<HashSet<_>>();
let vertices = solid
.vertices
.iter()
.filter(|vertex| vertex_ids.contains(&vertex.id))
.cloned()
.collect();
Ok(BrepSolid {
mass_properties_cache: Default::default(),
cells: Default::default(),
id: solid.id,
vertices,
edges,
shells: vec![ShellRecord {
id: 1,
faces: vec![face],
}],
genus: 0,
kind: crate::BodyKind::Solid,
})
}
pub(super) fn offset_support_collapsed(face: &FaceRecord, distance: f64) -> Result<bool, KernelRefusal> {
use crate::AnalyticSurface;
if face.surface.analytic().is_none() {
if let Some(factor) = certified_sphere_offset_factor(face, distance)? {
return Ok(factor <= COLLAPSE_FACTOR);
}
}
let (center, axis, radius) = match face.surface.analytic() {
Some(AnalyticSurface::Sphere { frame, radius }) => (frame.origin, None, *radius),
Some(AnalyticSurface::RuledRevolution {
frame, rho0, rho1, ..
}) if (rho0 - rho1).abs() <= 1e-9 * rho0.abs().max(1.0) => {
(frame.origin, Some(frame.axis), (rho0 + rho1) * 0.5)
}
Some(AnalyticSurface::Revolution {
frame, generatrix, ..
}) if generatrix.degree == 1 && generatrix.control_points.len() == 2 => {
let a = generatrix.control_points[0].point().or_refuse(KernelStage::Refine, "point")?.sub(frame.origin);
let b = generatrix.control_points[1].point().or_refuse(KernelStage::Refine, "point")?.sub(frame.origin);
let ra = a.sub(frame.axis.scale(a.dot(frame.axis)));
let rb = b.sub(frame.axis.scale(b.dot(frame.axis)));
if ra.sub(rb).length() > 1e-9 * ra.length().max(1.0) {
return Ok(false);
}
(frame.origin, Some(frame.axis), ra.length())
}
_ => return offset_curvature_collapsed(face, distance),
};
let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let (u, v) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
let mut radial = face.surface.evaluate(u, v).or_refuse(KernelStage::Refine, "evaluate")?.sub(center);
if let Some(axis) = axis {
radial = radial.sub(axis.scale(radial.dot(axis)));
}
let normal = face.surface.normal(u, v).or_refuse(KernelStage::Refine, "normal")?;
let sense = if face.same_sense { 1.0 } else { -1.0 };
let inward_change = distance * sense * normal.dot(radial.normalized().or_refuse(KernelStage::Refine, "normalized")?);
Ok(radius - inward_change <= 1e-9 * radius.max(distance.abs()).max(1.0))
}
pub(super) const COLLAPSE_FACTOR: f64 = 1e-6;
fn certified_sphere_offset_factor(face: &FaceRecord, distance: f64) -> Result<Option<f64>, KernelRefusal> {
if std::env::var("BREP_OFFSET_FOLD_CHART").as_deref() == Ok("0") {
return Ok(None);
}
let mut geometry = face.surface.analytic().and_then(|shape| shape.sphere_geometry());
if geometry.is_none() && face.surface.degree_u == 2 && face.surface.degree_v == 2 {
let transposed = crate::offset::transpose_offset_surface(&face.surface)?;
geometry = transposed.analytic().and_then(|shape| shape.sphere_geometry());
}
let Some((center, radius)) = geometry else { return Ok(None); };
let [u0,u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0,v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let sample = crate::OffsetEvaluator::new("offset_shell", &face.surface,
crate::OffsetNormal::FaceStable { same_sense: face.same_sense })
.at((u0+u1)*0.5, (v0+v1)*0.5, -distance).or_refuse(KernelStage::Refine, "at")?;
let alignment = sample.normal.dot(sample.source.sub(center)) / radius;
if alignment.abs() < 0.5 { return Ok(None); }
let factor = 1.0 - distance * alignment.signum() / radius;
os_debug!("certified sphere factor face {}: {factor:.12} radius={radius:.12}", face.id);
Ok(Some(factor))
}
pub(super) fn shell_displacement(face: &FaceRecord, distance: f64) -> f64 {
let sense = if face.same_sense { 1.0 } else { -1.0 };
-distance * sense
}
fn offset_curvature_collapsed(face: &FaceRecord, distance: f64) -> Result<bool, KernelRefusal> {
if distance == 0.0 || face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
return Ok(false);
}
let displacement = shell_displacement(face, distance);
let region = TrimRegion::from_face(face)?;
let scan = scan_offset_regularity(
&face.surface,
®ion,
&[displacement],
COLLAPSE_FACTOR,
ScanBudget::SHELL_FACE,
)?;
os_debug!(
"curvature collapse probe face {}: worst factor {:.6} at {} over {}/{} collapsed samples \
inside the trim",
face.id,
scan.worst.map(|worst| worst.factor).unwrap_or(f64::INFINITY),
scan.worst
.map(|worst| format!("(u={:.6}, v={:.6})", worst.u, worst.v))
.unwrap_or_else(|| "nowhere".to_string()),
scan.collapsed,
scan.sampled
);
if scan.sampled > 0 && scan.collapsed == scan.sampled && scan.between_regular == 0 {
return Ok(true);
}
if scan.collapsed > 0 && offset_sections_collapsed(face, displacement)? {
return Ok(true);
}
Ok(false)
}
fn offset_sections_collapsed(face: &FaceRecord, displacement: f64) -> Result<bool, KernelRefusal> {
let surface = &face.surface;
let tolerance = KernelTolerances::default().pcurve_consistency;
let [u0, u1, v0, v1] = TrimRegion::from_face(face)?.bounds();
if surface.degree_v != 2 || surface.control_points[0].len() != 3 {
return Ok(false);
}
let mut sampled = false;
for span in surface.knots_u.windows(2) {
let lo = span[0].max(u0);
let hi = span[1].min(u1);
if hi <= lo {
continue;
}
for i in 0..=4 {
let u = lo + (hi - lo) * i as f64 / 4.0;
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut high = low.scale(-1.0);
let first = surface.evaluate(u, v0).or_refuse(KernelStage::Refine, "evaluate")?;
let last = surface.evaluate(u, v1).or_refuse(KernelStage::Refine, "evaluate")?;
if last.sub(first).length() <= 2.0 * tolerance {
return Ok(false);
}
for j in 0..=16 {
let v = v0 + (v1 - v0) * j as f64 / 16.0;
let Ok(point) = surface.evaluate(u, v) else {
return Ok(false);
};
let Ok(normal) = surface.normal(u, v) else {
return Ok(false);
};
let image = point.add(normal.scale(displacement));
low = Vec3::new(low.x.min(image.x), low.y.min(image.y), low.z.min(image.z));
high = Vec3::new(high.x.max(image.x), high.y.max(image.y), high.z.max(image.z));
if high.sub(low).length() > 2.0 * tolerance {
return Ok(false);
}
}
sampled = true;
}
}
Ok(sampled)
}
pub(super) struct CarvedSupport {
pub(super) solid: BrepSolid,
pub(super) face: FaceRecord,
pub(super) kept_fraction: f64,
pub(super) fold_boundary: usize,
pub(super) shielded_cusp: bool,
}
fn cylinder_carve_cert_on() -> bool {
switch!("BREP_OFFSET_CYLINDER_CARVE_CERT", true)
}
pub(super) fn carve_folded_support(
source: &BrepSolid,
face: &FaceRecord,
distance: f64,
) -> Result<Option<CarvedSupport>, KernelRefusal> {
carve_folded_support_impl(source, face, distance, None)
}
pub(super) fn carve_shell_support(
source: &BrepSolid,
face: &FaceRecord,
distance: f64,
openings: &[u64],
) -> Result<Option<CarvedSupport>, KernelRefusal> {
carve_folded_support_impl(source, face, distance, Some(openings))
}
fn carve_folded_support_impl(
source: &BrepSolid,
face: &FaceRecord,
distance: f64,
openings: Option<&[u64]>,
) -> Result<Option<CarvedSupport>, KernelRefusal> {
if distance == 0.0 || face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
return Ok(None);
}
if certified_sphere_offset_factor(face, distance)?.is_some_and(|factor| factor > COLLAPSE_FACTOR) {
return Ok(None);
}
if cylinder_carve_cert_on() {
if let Some((_, _, _, factor)) = crate::offset::cylinder_offset_factor(&face.surface, face.same_sense, distance)? {
if factor > COLLAPSE_FACTOR {
os_debug!("certified cylinder factor face {}: {factor:.12}", face.id);
return Ok(None);
}
}
}
let loops = face
.loops
.iter()
.map(|loop_record| {
loop_record
.coedges
.iter()
.map(|coedge| coedge.pcurve.clone())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let displacement = shell_displacement(face, distance);
let mut outcome = carve_folded_trim_scanned(
&face.surface,
&loops,
&[displacement],
COLLAPSE_FACTOR,
ScanBudget::SHELL_FACE,
)?;
let Some(carved) = outcome.carved.take() else {
return match outcome.scan.certify(&face.surface, &outcome.region, &[displacement], COLLAPSE_FACTOR) {
Certainty::Regular => Ok(None),
Certainty::Folded(sample) => Err(super::refusals::fold_between_samples(face.id, distance, sample)),
Certainty::Uncertain {
unresolved,
budget_exhausted,
least,
} => Err(super::refusals::regularity_uncertain(
face.id,
distance,
unresolved,
budget_exhausted,
least,
)),
};
};
let mut solid = source.clone();
let mut next_id = solid
.vertices
.iter()
.map(|vertex| vertex.id)
.chain(solid.edges.iter().map(|edge| edge.id))
.chain(
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| {
std::iter::once(loop_record.id)
.chain(loop_record.coedges.iter().map(|coedge| coedge.id))
}),
)
.max()
.unwrap_or(0)
+ 1;
if carved.kept.len() > 1 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_fold_band", format!(
"offset_shell: the offset of source face {} folds in a BAND across its trim. The \
fold locus divides it into {} regular piece(s) with {} folded one(s) dropped \
between them, and thicken builds a body per piece — but this lane keeps the source \
SKIN whole, out to the face's own rim, and over a dropped band that skin has no \
offset image to close against. The shell refuses rather than assemble one it \
cannot close",
face.id,
carved.kept.len(),
carved.dropped.len()
)));
}
let (fold_image_extent, fold_image_bar) = fold_image_extent(source, face, &carved.kept[0], distance)?;
let shielded = if fold_image_extent > fold_image_bar && distance > 0.0 {
match openings {
Some(openings) => fold_image_shielded(source, face, &carved.kept[0], distance, openings)?,
None => false,
}
} else {
false
};
if fold_image_extent > fold_image_bar && !shielded {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "offset_shell_cusp", format!(
"offset_shell: the offset of source face {} folds across part of its trim, and the \
fold locus that bounds the kept part is not a PINCH — its offset image spans \
{fold_image_extent:.6e} against the {fold_image_bar:.3e} a pinch can reach at the \
fold level. The cavity would end on a CUSP edge of the offset, which has to be \
re-solved against the offset's own other sheet, and nothing in this lane does; the \
shell refuses rather than end the cavity there ({} kept piece, {} folded one(s) \
dropped)",
face.id,
carved.kept.len(),
carved.dropped.len()
)));
}
let mut fold_boundary = 0usize;
let mut new_loops = Vec::with_capacity(carved.kept[0].len());
for carved_loop in &carved.kept[0] {
let count = carved_loop.pcurves.len();
if count == 0 {
return Err(KernelRefusal::internal(KernelStage::Refine, "offset_shell_carve_empty_loop", "offset_shell: the carve produced an empty loop"));
}
let mut vertex_ids = Vec::with_capacity(count);
for pcurve in &carved_loop.pcurves {
let [t0, _] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let uv = pcurve.evaluate(t0).or_refuse(KernelStage::Refine, "evaluate")?;
solid.vertices.push(VertexRecord {
id: next_id,
point: face.surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")?,
});
vertex_ids.push(next_id);
next_id += 1;
}
let mut coedges = Vec::with_capacity(count);
for (index, pcurve) in carved_loop.pcurves.iter().enumerate() {
let source_edge = carved_loop.origin[index].and_then(|(loop_index, coedge_index)| {
let coedge = face.loops.get(loop_index)?.coedges.get(coedge_index)?;
source.edges.iter().find(|edge| edge.id == coedge.edge_id)
});
if carved_loop.origin[index].is_none() {
fold_boundary += 1;
}
let degenerate = source_edge.is_some_and(|edge| edge.degenerate);
let curve = source_image_curve(&face.surface, pcurve, degenerate)?;
let [t0, t1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let edge_id = next_id;
next_id += 1;
solid.edges.push(EdgeRecord {
id: edge_id,
curve,
t0,
t1,
start_vertex_id: vertex_ids[index],
end_vertex_id: vertex_ids[(index + 1) % count],
degenerate,
name: source_edge.and_then(|edge| edge.name.clone()),
});
coedges.push(CoedgeRecord {
id: next_id,
edge_id,
forward: true,
pcurve: pcurve.clone(),
});
next_id += 1;
}
new_loops.push(LoopRecord {
id: next_id,
coedges,
});
next_id += 1;
}
let mut carved_face = face.clone();
carved_face.loops = new_loops;
if shielded {
carved_face.surface = support_over_trim(&carved_face)?;
}
for shell in &mut solid.shells {
for existing in &mut shell.faces {
if existing.id == face.id {
*existing = carved_face.clone();
}
}
}
Ok(Some(CarvedSupport {
solid,
face: carved_face,
kept_fraction: carved.kept_fraction,
fold_boundary,
shielded_cusp: shielded,
}))
}
fn support_over_trim(face: &FaceRecord) -> Result<NurbsSurface, KernelRefusal> {
let surface = &face.surface;
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let mut bounds = [u1, u0, v1, v0];
for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
for control in &coedge.pcurve.control_points {
let uv = control.point().or_refuse(KernelStage::Refine, "point")?;
bounds[0] = bounds[0].min(uv.x);
bounds[1] = bounds[1].max(uv.x);
bounds[2] = bounds[2].min(uv.y);
bounds[3] = bounds[3].max(uv.y);
}
}
let restrict = |mut curve: NurbsCurve, low: f64, high: f64| -> Result<NurbsCurve, KernelRefusal> {
let [a, b] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let band = (b - a) * 1e-12;
if high < b - band {
curve = curve.split(high.max(a)).or_refuse(KernelStage::Refine, "split")?.0;
}
if low > a + band {
curve = curve.split(low.min(b)).or_refuse(KernelStage::Refine, "split")?.1;
}
Ok(curve)
};
let columns = (0..surface.control_points[0].len()).map(|j| {
let curve = NurbsCurve::new(surface.degree_u, surface.knots_u.clone(),
surface.control_points.iter().map(|row| row[j]).collect())
.or_refuse(KernelStage::Refine, "new")?;
restrict(curve, bounds[0], bounds[1])
}).collect::<Result<Vec<_>, KernelRefusal>>()?;
let rows = (0..columns[0].control_points.len()).map(|i| {
let curve = NurbsCurve::new(surface.degree_v, surface.knots_v.clone(),
columns.iter().map(|column| column.control_points[i]).collect())
.or_refuse(KernelStage::Refine, "new")?;
restrict(curve, bounds[2], bounds[3])
}).collect::<Result<Vec<_>, KernelRefusal>>()?;
NurbsSurface::new(surface.degree_u, surface.degree_v, columns[0].knots.clone(),
rows[0].knots.clone(), rows.into_iter().map(|row| row.control_points).collect())
.or_refuse(KernelStage::Refine, "new")
}
fn fold_image_shielded(
source: &BrepSolid,
face: &FaceRecord,
kept: &[crate::offset_carve::CarvedLoop],
distance: f64,
openings: &[u64],
) -> Result<bool, KernelRefusal> {
let planes = source.shells.iter().flat_map(|shell| &shell.faces)
.filter(|other| other.id != face.id && !openings.contains(&other.id))
.filter(|other| other.surface.is_affine().unwrap_or(false))
.map(|other| {
let edges = source.edges.iter().filter(|edge| other.loops.iter()
.flat_map(|lp| &lp.coedges).any(|coedge| coedge.edge_id == edge.id)).collect::<Vec<_>>();
(other, edges)
}).collect::<Vec<_>>();
let tolerance = KernelTolerances::for_solid(source, 1e-7).model;
let displacement = shell_displacement(face, distance);
let mut covered = false;
for lp in kept {
for (curve, origin) in lp.pcurves.iter().zip(&lp.origin) {
if origin.is_some() { continue; }
let [a, b] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
for index in 0..=64 {
let uv = curve.evaluate(a + (b-a) * index as f64 / 64.0)
.or_refuse(KernelStage::Refine, "evaluate")?;
let (low, high) = face.surface.principal_curvatures(uv.x, uv.y)
.or_refuse(KernelStage::Refine, "principal_curvatures")?;
let outward_max = if face.same_sense { high } else { -low };
if !outward_max.is_finite() || distance * outward_max > COLLAPSE_FACTOR {
return Ok(false);
}
let point = face.surface.evaluate(uv.x, uv.y)
.or_refuse(KernelStage::Refine, "evaluate")?
.add(face.surface.normal(uv.x, uv.y)
.or_refuse(KernelStage::Refine, "normal")?.scale(displacement));
let mut nearest = f64::INFINITY;
for (plane, edges) in &planes {
if let Some(separation) = miter_distance_to_trimmed_face(plane, edges, point)? {
nearest = nearest.min(separation);
}
}
os_debug!("fold shielding face {} sample {index}: {nearest:.12} / {distance:.12}", face.id);
if nearest > distance + tolerance { return Ok(false); }
covered |= nearest < distance - tolerance;
}
}
}
Ok(covered)
}
fn fold_image_extent(
source: &BrepSolid,
face: &FaceRecord,
kept: &[crate::offset_carve::CarvedLoop],
distance: f64,
) -> Result<(f64, f64), KernelRefusal> {
const SAMPLES: usize = 32;
let bar = 2.0 * 3.0f64.sqrt() * COLLAPSE_FACTOR * crate::solid_scale(source)
+ KernelTolerances::for_solid(source, 1e-7).model;
let displacement = shell_displacement(face, distance);
let mut worst = 0.0f64;
for carved_loop in kept {
for (pcurve, origin) in carved_loop.pcurves.iter().zip(&carved_loop.origin) {
if origin.is_some() {
continue;
}
let [t0, t1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let (mut lo, mut hi) = (
Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
);
for index in 0..=SAMPLES {
let uv = pcurve.evaluate(t0 + (t1 - t0) * index as f64 / SAMPLES as f64).or_refuse(KernelStage::Refine, "evaluate")?;
let point = face
.surface
.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")?
.add(face.surface.normal(uv.x, uv.y).or_refuse(KernelStage::Refine, "normal")?.scale(displacement));
lo = Vec3::new(lo.x.min(point.x), lo.y.min(point.y), lo.z.min(point.z));
hi = Vec3::new(hi.x.max(point.x), hi.y.max(point.y), hi.z.max(point.z));
}
worst = worst.max(hi.sub(lo).length());
}
}
os_debug!(
"carve of source face {}: fold boundary image extent {worst:.6e} against the pinch bar \
{bar:.3e}",
face.id
);
Ok((worst, bar))
}
fn source_image_curve(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
degenerate: bool,
) -> Result<NurbsCurve, KernelRefusal> {
let [t0, t1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
const SAMPLES: usize = 64;
let mut points = Vec::with_capacity(SAMPLES + 1);
for index in 0..=SAMPLES {
let uv = pcurve.evaluate(t0 + (t1 - t0) * index as f64 / SAMPLES as f64).or_refuse(KernelStage::Refine, "evaluate")?;
points.push(surface.evaluate(uv.x, uv.y).or_refuse(KernelStage::Refine, "evaluate")?);
}
if degenerate {
return crate::make_line(points[0], points[points.len() - 1]).or_refuse(KernelStage::Refine, "make_line");
}
let mut parameters = Vec::with_capacity(points.len());
let mut total = 0.0;
parameters.push(0.0);
for window in points.windows(2) {
total += window[1].sub(window[0]).length().max(1e-12);
parameters.push(total);
}
crate::interpolate_curve(&points, 3, ¶meters).or_refuse(KernelStage::Refine, "interpolate_curve")
}
pub(super) fn carrier_solid(
source: &BrepSolid,
face_id: u64,
distance: f64,
planar_extension: f64,
) -> Result<BrepSolid, KernelRefusal> {
carrier_solid_sided(
source,
face_id,
distance,
&crate::CarrierExtension::uniform(planar_extension),
)
}
pub(super) fn carrier_solid_sided(
source: &BrepSolid,
face_id: u64,
distance: f64,
extension: &crate::CarrierExtension,
) -> Result<BrepSolid, KernelRefusal> {
carrier_solid_sided_lane(source, face_id, distance, extension).map(|(solid, _)| solid)
}
pub(super) fn carrier_solid_sided_lane(
source: &BrepSolid,
face_id: u64,
distance: f64,
extension: &crate::CarrierExtension,
) -> Result<(BrepSolid, crate::OffsetSurfaceLane), KernelRefusal> {
let carrier = crate::offset_face_carrier_sided(source, face_id, distance, extension)?;
let lane = carrier.lane;
Ok((
BrepSolid {
mass_properties_cache: Default::default(),
cells: Default::default(),
id: 1,
vertices: carrier.vertices,
edges: carrier.edges,
shells: vec![ShellRecord {
id: 1,
faces: vec![carrier.face],
}],
genus: 0,
kind: crate::BodyKind::Solid,
},
lane,
))
}
pub(super) fn drop_wall_interior_loops(wall: &mut BrepSolid) -> Result<(), KernelRefusal> {
let face = &mut wall.shells[0].faces[0];
if face.loops.len() < 2 {
return Ok(());
}
let mut outer = 0usize;
let mut outer_area = f64::NEG_INFINITY;
for (index, loop_record) in face.loops.iter().enumerate() {
let area = parameter_space_area(&FaceRecord {
id: 0,
surface: face.surface.clone(),
same_sense: true,
loops: vec![loop_record.clone()],
name: None,
}).or_refuse(KernelStage::Refine, "parameter_space_area")?
.abs();
if area > outer_area {
outer_area = area;
outer = index;
}
}
let kept = face.loops[outer].clone();
face.loops = vec![kept];
let used_edges = face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
.collect::<HashSet<_>>();
wall.edges.retain(|edge| used_edges.contains(&edge.id));
let used_vertices = wall
.edges
.iter()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect::<HashSet<_>>();
wall.vertices
.retain(|vertex| used_vertices.contains(&vertex.id));
Ok(())
}
fn merge_vertex(
vertices: &mut Vec<ImprintVertex>,
point: Vec3,
next_id: &mut u64,
tolerance: f64,
scale: f64,
) -> u64 {
let merge = tolerance.max(1e-5) * crate::tolerance::merge_scale(scale);
if let Some(vertex) = vertices
.iter()
.find(|vertex| vertex.point.sub(point).length() <= merge)
{
return vertex.id;
}
let id = *next_id;
*next_id += 1;
vertices.push(ImprintVertex { id, point });
id
}
pub(super) fn merge_pair_imprint(
global: &mut ImprintResultRecord,
pair: ImprintResultRecord,
first_operand: u8,
second_operand: u8,
next_piece_id: &mut u64,
next_vertex_id: &mut u64,
tolerance: f64,
scale: f64,
) {
let remap_operand = |operand| {
if operand == 0 {
first_operand
} else {
second_operand
}
};
let mut vertex_map = HashMap::default();
for vertex in pair.vertices {
vertex_map.insert(
vertex.id,
merge_vertex(
&mut global.vertices,
vertex.point,
next_vertex_id,
tolerance,
scale,
),
);
}
let mut piece_map = HashMap::default();
for piece in pair.pieces {
let id = *next_piece_id;
*next_piece_id += 1;
piece_map.insert(piece.id, id);
global.pieces.push(ImprintPieceRecord {
id,
curve: piece.curve,
t0: piece.t0,
t1: piece.t1,
start_vertex_id: vertex_map[&piece.start_vertex_id],
end_vertex_id: vertex_map[&piece.end_vertex_id],
pcurves: piece
.pcurves
.into_iter()
.map(|pcurve| FacePcurve {
operand: remap_operand(pcurve.operand),
face_id: pcurve.face_id,
pcurve: pcurve.pcurve,
})
.collect(),
support_faces: piece.support_faces.map(|face| FaceKey {
operand: remap_operand(face.operand),
face_id: face.face_id,
}),
shared_edge: piece
.shared_edge
.map(|(operand, edge_id, aligned)| (remap_operand(operand), edge_id, aligned)),
});
}
for by_face in pair.by_face {
let operand = remap_operand(by_face.operand);
let entry = if let Some(entry) = global
.by_face
.iter_mut()
.find(|entry| entry.operand == operand && entry.face_id == by_face.face_id)
{
entry
} else {
global.by_face.push(FaceImprints {
operand,
face_id: by_face.face_id,
piece_ids: Vec::new(),
});
global.by_face.last_mut().unwrap()
};
entry.piece_ids.extend(
by_face
.piece_ids
.into_iter()
.filter_map(|id| piece_map.get(&id).copied()),
);
}
for split in pair.edge_splits {
let operand = remap_operand(split.operand);
if let Some(existing) = global
.edge_splits
.iter_mut()
.find(|entry| entry.operand == operand && entry.edge_id == split.edge_id)
{
for parameter in split.parameters {
if !existing
.parameters
.iter()
.any(|value| (*value - parameter).abs() <= 1e-8)
{
existing.parameters.push(parameter);
}
}
} else {
global.edge_splits.push(EdgeSplitRecord {
operand,
edge_id: split.edge_id,
parameters: split.parameters,
});
}
}
}
pub(super) fn empty_imprint() -> ImprintResultRecord {
ImprintResultRecord {
vertices: Vec::new(),
pieces: Vec::new(),
by_face: Vec::new(),
edge_splits: Vec::new(),
barrier_edges: Vec::new(),
tangent_nodes: Vec::new(),
section_evidence: false,
cosurface_pairs: Vec::new(),
tangent_sections: Vec::new(),
}
}
pub(super) fn flip_fragment(fragment: &mut FaceFragmentRecord) -> Result<(), KernelRefusal> {
fragment.same_sense = !fragment.same_sense;
for loop_record in &mut fragment.loops {
loop_record.coedges.reverse();
for coedge in &mut loop_record.coedges {
coedge.forward = !coedge.forward;
coedge.pcurve = coedge.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
}
}
Ok(())
}
pub(super) fn fragment_as_trim(fragment: &FaceFragmentRecord) -> FaceRecord {
FaceRecord {
id: fragment.source_face_id,
surface: fragment.surface.clone(),
same_sense: fragment.same_sense,
loops: fragment
.loops
.iter()
.enumerate()
.map(|(loop_index, loop_record)| LoopRecord {
id: loop_index as u64 + 1,
coedges: loop_record
.coedges
.iter()
.enumerate()
.map(|(index, coedge)| CoedgeRecord {
id: index as u64 + 1,
edge_id: index as u64 + 1,
forward: coedge.forward,
pcurve: coedge.pcurve.clone(),
})
.collect(),
})
.collect(),
name: None,
}
}
fn skin_distance_to_trimmed_face(
face: &FaceRecord,
edges: &[&EdgeRecord],
point: Vec3,
) -> Result<f64, KernelRefusal> {
match miter_distance_to_trimmed_face(face, edges, point)? {
Some(measured) => Ok(measured),
None => distance_to_trimmed_face(face, edges, point),
}
}
pub(super) fn distance_to_trimmed_face(
face: &FaceRecord,
edges: &[&EdgeRecord],
point: Vec3,
) -> Result<f64, KernelRefusal> {
Ok(nearest_on_trimmed_face(face, edges, point)?.0)
}
fn miter_distance_to_trimmed_face(
face: &FaceRecord,
edges: &[&EdgeRecord],
point: Vec3,
) -> Result<Option<f64>, KernelRefusal> {
if !face.surface.is_affine().or_refuse(KernelStage::Refine, "is_affine")? {
return Ok(None);
}
let [u0, u1] = face.surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = face.surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let origin = face.surface.evaluate(u0, v0).or_refuse(KernelStage::Refine, "evaluate")?;
let along_u = face.surface.evaluate(u1, v0).or_refuse(KernelStage::Refine, "evaluate")?.sub(origin);
let along_v = face.surface.evaluate(u0, v1).or_refuse(KernelStage::Refine, "evaluate")?.sub(origin);
let normal = along_u.cross(along_v);
let normal_length = normal.length();
if !(normal_length > 0.0) {
return Ok(None);
}
let normal = normal.scale(1.0 / normal_length);
let relative = point.sub(origin);
let height = relative.dot(normal);
let foot = point.sub(normal.scale(height));
let (g11, g12, g22) = (
along_u.dot(along_u),
along_u.dot(along_v),
along_v.dot(along_v),
);
let determinant = g11 * g22 - g12 * g12;
if !(determinant.abs() > 0.0) {
return Ok(None);
}
let offset_from_origin = foot.sub(origin);
let (r1, r2) = (along_u.dot(offset_from_origin), along_v.dot(offset_from_origin));
let uv = Vec2 {
x: u0 + (u1 - u0) * (r1 * g22 - r2 * g12) / determinant,
y: v0 + (v1 - v0) * (r2 * g11 - r1 * g12) / determinant,
};
if parameter_point_in_face(face, uv, 1e-9).or_refuse(KernelStage::Refine, "parameter_point_in_face")? != PolygonClass::Outside {
return Ok(Some(height.abs()));
}
let mut tangential = f64::INFINITY;
for edge in edges {
if edge.degenerate {
continue;
}
if segment_miter_enabled() {
if let Some(distance) = segment_edge_distance(&edge.curve, edge.t0, edge.t1, foot) {
tangential = tangential.min(distance);
continue;
}
}
let projection = crate::project_point_to_curve(&edge.curve, foot).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let parameter = projection.u.clamp(edge.t0, edge.t1);
tangential = tangential.min(edge.curve.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?.sub(foot).length());
}
if !tangential.is_finite() {
return Ok(None);
}
Ok(Some(height.abs().max(tangential)))
}
fn segment_edge_distance(curve: &NurbsCurve, t0: f64, t1: f64, point: Vec3) -> Option<f64> {
if curve.degree != 1 || curve.control_points.len() != 2 || curve.knots.len() != 4 {
return None;
}
let [a, b] = [curve.control_points[0], curve.control_points[1]];
if !(a.w > 0.0) || a.w != b.w {
return None;
}
let (start, end) = (a.point().ok()?, b.point().ok()?);
let [k0, k1] = curve.domain().ok()?;
let direction = end.sub(start);
let length_squared = direction.dot(direction);
if !(length_squared > 0.0) || !(k1 > k0) {
return None;
}
let along = (point.sub(start).dot(direction) / length_squared).clamp(0.0, 1.0);
let parameter = (k0 + (k1 - k0) * along).clamp(t0.min(t1), t0.max(t1));
let nearest = start.add(direction.scale((parameter - k0) / (k1 - k0)));
Some(nearest.sub(point).length())
}
fn segment_miter_enabled() -> bool {
switch!("BREP_OFFSET_SEGMENT_MITER", true)
}
pub(super) fn own_miter_departure(
point: Vec3,
own_face_id: u64,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
) -> Option<f64> {
let (face, edges) = retained_faces.iter().find(|(face, _)| face.id == own_face_id)?;
let measured = miter_distance_to_trimmed_face(face, edges, point).ok()??;
Some(measured - distance.abs())
}
pub(super) fn point_adrift_of_its_own_offset(
point: Vec3,
own_face_id: u64,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
tolerance: f64,
) -> bool {
own_miter_departure(point, own_face_id, retained_faces, distance)
.is_some_and(|departure| departure > tolerance)
}
fn nearest_on_trimmed_face(
face: &FaceRecord,
edges: &[&EdgeRecord],
point: Vec3,
) -> Result<(f64, Option<Vec3>), KernelRefusal> {
let projection = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let uv = Vec2 {
x: projection.u,
y: projection.v,
};
if parameter_point_in_face(face, uv, 1e-9).or_refuse(KernelStage::Refine, "parameter_point_in_face")? != PolygonClass::Outside {
return Ok((projection.distance, Some(projection.point)));
}
let mut best = (f64::INFINITY, None);
for edge in edges {
if edge.degenerate {
continue;
}
let samples = 48;
for index in 0..=samples {
let parameter = edge.t0 + (edge.t1 - edge.t0) * index as f64 / samples as f64;
let sample = edge.curve.evaluate(parameter).or_refuse(KernelStage::Refine, "evaluate")?;
let separation = sample.sub(point).length();
if separation < best.0 {
best = (separation, Some(sample));
}
}
}
Ok(best)
}
pub(super) fn coincident_offset_face(
point: Vec3,
own_face_id: u64,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
tolerance: f64,
) -> Result<Option<(u64, f64)>, KernelRefusal> {
let Some(own_foot) = retained_foot(point, own_face_id, retained_faces)? else {
return Ok(None);
};
let target = distance.abs();
let bounded = coincident_bound_on();
for (face, edges) in retained_faces {
if face.id == own_face_id {
continue;
}
if bounded && foot_box_distance(face, edges, point).is_some_and(|reach| reach > beyond_band(target + tolerance, point)) {
continue;
}
let (separation, Some(foot)) = nearest_on_trimmed_face(face, edges, point)? else {
continue;
};
if (separation - target).abs() <= tolerance
&& point.sub(own_foot).dot(point.sub(foot)) < 0.0
{
return Ok(Some((face.id, separation)));
}
}
Ok(None)
}
fn coincident_bound_on() -> bool {
switch!("BREP_OFFSET_COINCIDENT_BOUND", true)
}
fn foot_box_distance(face: &FaceRecord, edges: &[&EdgeRecord], point: Vec3) -> Option<f64> {
let (mut low, mut high) = (Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY), Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY));
let surface_points = face.surface.control_points.iter().flatten();
let edge_points = edges.iter().filter(|edge| !edge.degenerate).flat_map(|edge| edge.curve.control_points.iter());
for control in surface_points.chain(edge_points) {
if !(control.w > 0.0 && control.w.is_finite()) {
return None;
}
let p = control.point().ok()?;
if !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite()) {
return None;
}
low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
}
if !(low.x <= high.x) {
return None;
}
let nearest = Vec3::new(point.x.clamp(low.x, high.x), point.y.clamp(low.y, high.y), point.z.clamp(low.z, high.z));
Some(point.sub(nearest).length())
}
fn beyond_band(reach: f64, point: Vec3) -> f64 {
reach * (1.0 + 1e-6) + 1e-6 * (1.0 + point.length())
}
pub(super) fn own_offset_departure(
point: Vec3,
own_face_id: u64,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
) -> Option<f64> {
let foot = retained_foot(point, own_face_id, retained_faces).ok()??;
Some(point.sub(foot).length() - distance.abs())
}
fn retained_foot(
point: Vec3,
face_id: u64,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
) -> Result<Option<Vec3>, KernelRefusal> {
let Some((face, edges)) = retained_faces.iter().find(|(face, _)| face.id == face_id) else {
return Ok(None);
};
Ok(nearest_on_trimmed_face(face, edges, point)?.1)
}
pub(super) fn crossing_face_at(
point: Vec3,
own_face_id: u64,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
tolerance: f64,
) -> Result<Option<(u64, f64)>, KernelRefusal> {
let Some(own) = retained_foot(point, own_face_id, retained_faces)? else {
return Ok(None);
};
let to_own = point.sub(own);
let reach = to_own.length();
if reach <= tolerance {
return Ok(None);
}
for (face, edges) in retained_faces {
if face.id == own_face_id {
continue;
}
let (separation, Some(foot)) = nearest_on_trimmed_face(face, edges, point)? else {
continue;
};
if separation <= tolerance || separation >= distance.abs() - tolerance {
continue;
}
if to_own.dot(point.sub(foot)) < -CROSSING_OPPOSITION * reach * separation {
return Ok(Some((face.id, separation)));
}
}
Ok(None)
}
pub(super) const CROSSING_OPPOSITION: f64 = 0.866;
pub(super) const MAXIMUM_CROSSING_READS: usize = 64;
pub(super) fn name_offset_crossing(
source: &BrepSolid,
opening_face_ids: &[u64],
distance: f64,
points: &[(Vec3, u64)],
) -> Option<String> {
let edges = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let retained = source
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| !opening_face_ids.contains(&face.id))
.map(|face| {
let face_edges = face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.filter_map(|coedge| edges.get(&coedge.edge_id).copied())
.collect::<Vec<_>>();
(face, face_edges)
})
.collect::<Vec<_>>();
let tolerance = offset_skin_band(crate::solid_scale(source), distance);
points.iter().find_map(|(point, own)| {
let (face, separation) = crossing_face_at(*point, *own, &retained, distance, tolerance).ok()??;
Some(format!(
"the offsets of source faces {own} and {face} cross near ({:.4}, {:.4}, {:.4}), where \
the material between them is thinner than twice the shell distance ({separation:.4} \
from face {face} against {:.4})",
point.x,
point.y,
point.z,
distance.abs(),
))
})
}
pub(super) fn offset_skin_band(scale: f64, distance: f64) -> f64 {
2e-3f64.max(scale * 5e-5).max(distance.abs() * 1e-3)
}
#[derive(Clone, Debug)]
pub(super) enum SkinReading {
On,
Shadowed { face: u64, separation: f64 },
Unmeasured { face: u64, reason: String },
}
pub(super) fn offset_skin_reading(
point: Vec3,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
tolerance: f64,
) -> SkinReading {
let target = distance.abs();
let mut unmeasured = None;
for (face, edges) in retained_faces {
match skin_distance_to_trimmed_face(face, edges, point) {
Ok(separation) if separation.is_finite() => {
if separation < target - tolerance {
return SkinReading::Shadowed {
face: face.id,
separation,
};
}
}
Ok(separation) => {
unmeasured.get_or_insert((face.id, format!("the distance read {separation}")));
}
Err(error) => {
unmeasured.get_or_insert((face.id, error.message));
}
}
}
match unmeasured {
Some((face, reason)) => SkinReading::Unmeasured { face, reason },
None => SkinReading::On,
}
}
pub(super) fn point_on_offset_skin(
point: Vec3,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
distance: f64,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
match offset_skin_reading(point, retained_faces, distance, tolerance) {
SkinReading::On => Ok(true),
SkinReading::Shadowed { .. } => Ok(false),
SkinReading::Unmeasured { face, reason } => Err(unmeasured_skin_refusal(point, face, &reason)),
}
}
pub(super) fn point_on_inward_opening_skin(
point: Vec3,
retained_faces: &[(&FaceRecord, Vec<&EdgeRecord>)],
reflex_neighbors: &HashSet<u64>,
inside_source: bool,
distance: f64,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
let mut material_side = Vec::new();
let mut reflex_side = Vec::new();
for (face, edges) in retained_faces {
if face
.surface
.is_affine()
.or_refuse(KernelStage::Select, "is_affine")?
{
let projection = project_point_to_surface(&face.surface, point)
.or_refuse(KernelStage::Select, "project_point_to_surface")?;
let foot = face
.surface
.evaluate(projection.u, projection.v)
.or_refuse(KernelStage::Select, "evaluate")?;
let normal = face_normal(face, projection.u, projection.v)?;
if point.sub(foot).dot(normal) > tolerance {
continue;
}
}
if inside_source && reflex_neighbors.contains(&face.id) {
reflex_side.push((*face, edges.clone()));
} else {
material_side.push((*face, edges.clone()));
}
}
let other_skin = point_on_offset_skin(point, &material_side, distance, tolerance)?;
if inside_source {
let reflex_skin = point_on_offset_skin(point, &reflex_side, distance, tolerance)?;
Ok(reflex_skin || !other_skin)
} else {
Ok(other_skin)
}
}
pub(super) fn opening_trim_halfspaces(
face: &FaceRecord,
openings: &[&FaceRecord],
tolerance: f64,
) -> Result<Vec<(Vec3, Vec3)>, KernelRefusal> {
let mut planes = Vec::new();
if !face
.surface
.is_affine()
.or_refuse(KernelStage::Select, "is_affine")?
{
return Ok(planes);
}
'opening: for opening in openings {
if !source_faces_adjacent(face, opening) {
continue;
}
let Some((origin, normal)) = planar_surface_frame(&opening.surface, tolerance)? else {
continue;
};
let mut low = f64::INFINITY;
let mut high = f64::NEG_INFINITY;
for control in face
.loops
.iter()
.flat_map(|l| &l.coedges)
.flat_map(|c| &c.pcurve.control_points)
{
if control.w <= 0.0 {
continue 'opening;
}
let Ok(point) = face
.surface
.evaluate(control.x / control.w, control.y / control.w)
else {
continue 'opening;
};
let side = point.sub(origin).dot(normal);
low = low.min(side);
high = high.max(side);
}
if low >= -tolerance && high > tolerance {
planes.push((origin, normal));
} else if high <= tolerance && low < -tolerance {
planes.push((origin, normal.scale(-1.0)));
}
}
Ok(planes)
}
pub(super) fn name_retained_opening_junction(
source: &BrepSolid,
opening_face_ids: &[u64],
distance: f64,
) -> Option<String> {
if distance <= 0.0 {
return None;
}
let faces = source
.shells
.iter()
.flat_map(|shell| &shell.faces)
.collect::<Vec<_>>();
let edges = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
for face in faces.iter().filter(|face| !opening_face_ids.contains(&face.id)) {
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
let Some(edge) = edges.get(&coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
let Some((mate, _)) = junction_mate(&faces, face, coedge.edge_id) else {
continue;
};
if !opening_face_ids.contains(&mate.id) {
continue;
}
let Ok(stations) = junction_stations(face, coedge, edge, mate) else {
continue;
};
let coplanar_planes = stations.is_empty()
&& face.surface.is_affine().unwrap_or(false)
&& mate.surface.is_affine().unwrap_or(false);
let reflex = stations.iter().any(|station| !station.is_convex());
if coplanar_planes || reflex {
return Some(format!(
"retained face {} meets opening {} along edge {} at a {} junction, so its \
offset ends inside the cavity beside the opening, where the wall needs a face \
no carrier supplies (on a partial revolve whose cap is retained, that offset \
also shadows the apex pinch and splits the offset skin into horns ending in \
tangencies)",
face.id,
mate.id,
edge.id,
if reflex { "REFLEX" } else { "FLAT (coplanar)" },
));
}
}
}
None
}
pub(super) fn unmeasured_skin_refusal(point: Vec3, face: u64, reason: &str) -> KernelRefusal {
KernelRefusal::internal(
KernelStage::Select,
"offset_shell_unmeasured_skin",
format!(
"offset_shell: cannot tell whether ({:.4}, {:.4}, {:.4}) lies on the offset skin: its \
distance to retained face {face} could not be measured ({reason})",
point.x, point.y, point.z
),
)
}
pub(super) fn source_seeds(source: &BrepSolid, face_id: u64) -> Result<Vec<Vec2>, KernelRefusal> {
let face = standalone_face(source, face_id)?;
let fragments = fragment_solid(&face, SOURCE_OPERAND, &empty_imprint())?;
if fragments.is_empty() {
return Err(KernelRefusal::internal(KernelStage::Select, "offset_shell_no_interior_seed", "offset_shell: source face has no interior seed"));
}
Ok(fragments.iter().map(|fragment| fragment.test_uv).collect())
}
pub(super) fn offset_seed(
source_face: &FaceRecord,
raw_seed: Vec2,
carrier_surface: &crate::NurbsSurface,
distance: f64,
band: f64,
) -> Result<Option<Vec2>, KernelRefusal> {
let sample = face_offsets(source_face).at(raw_seed.x, raw_seed.y, -distance).or_refuse(KernelStage::Refine, "at")?;
let projection = project_point_to_surface(carrier_surface, sample.point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
if !projection.distance.is_finite() || projection.distance > band {
return Ok(None);
}
Ok(Some(Vec2 {
x: projection.u,
y: projection.v,
}))
}
#[derive(Clone)]
pub(super) struct FragmentEdgeGeometry {
pub(super) curve: crate::NurbsCurve,
pub(super) t0: f64,
pub(super) t1: f64,
}
pub(super) fn fragment_edge_segment_key(
source: &FragmentEdgeSource,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
) -> Result<[i64; 9], KernelRefusal> {
let geometry = fragment_edge_geometry(source, solids, imprint)?;
let mut start = geometry.curve.evaluate(geometry.t0).or_refuse(KernelStage::Refine, "evaluate")?;
let middle = geometry.curve.evaluate((geometry.t0 + geometry.t1) * 0.5).or_refuse(KernelStage::Refine, "evaluate")?;
let mut end = geometry.curve.evaluate(geometry.t1).or_refuse(KernelStage::Refine, "evaluate")?;
if (end.x, end.y, end.z) < (start.x, start.y, start.z) {
std::mem::swap(&mut start, &mut end);
}
let quantize = |value: f64| (value / 1e-6).round() as i64;
Ok([
quantize(start.x),
quantize(start.y),
quantize(start.z),
quantize(middle.x),
quantize(middle.y),
quantize(middle.z),
quantize(end.x),
quantize(end.y),
quantize(end.z),
])
}
fn fragment_edge_geometry(
source: &FragmentEdgeSource,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
) -> Result<FragmentEdgeGeometry, KernelRefusal> {
match source {
FragmentEdgeSource::Boundary { operand, edge_id }
| FragmentEdgeSource::SharedBoundary { operand, edge_id } => {
let edge = solids
.get(operand)
.and_then(|solid| solid.edges.iter().find(|edge| edge.id == *edge_id))
.ok_or_else(|| {
format!("offset_shell: missing boundary edge {operand}:{edge_id}")
}).or_refuse(KernelStage::Refine, "offset_shell_missing_boundary_edge")?;
Ok(FragmentEdgeGeometry {
curve: edge.curve.clone(),
t0: edge.t0,
t1: edge.t1,
})
}
FragmentEdgeSource::Imprint { piece_id } => {
let piece = imprint
.pieces
.iter()
.find(|piece| piece.id == *piece_id)
.ok_or_else(|| format!("offset_shell: missing imprint piece {piece_id}")).or_refuse(KernelStage::Refine, "offset_shell_missing_imprint_piece")?;
Ok(FragmentEdgeGeometry {
curve: piece.curve.clone(),
t0: piece.t0,
t1: piece.t1,
})
}
FragmentEdgeSource::Derived { curve, t0, t1, .. } => Ok(FragmentEdgeGeometry {
curve: curve.clone(),
t0: *t0,
t1: *t1,
}),
}
}
pub(super) fn fragment_edge_lies_on(
source: &FragmentEdgeGeometry,
target: &FragmentEdgeGeometry,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
for fraction in [0.0, 0.25, 0.5, 0.75, 1.0] {
let point = source
.curve
.evaluate(source.t0 + (source.t1 - source.t0) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let projection = crate::project_point_to_curve(&target.curve, point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
if projection.distance > tolerance
|| projection.u < target.t0 - tolerance
|| projection.u > target.t1 + tolerance
{
return Ok(false);
}
}
Ok(true)
}
fn fragment_edges_overlap(
first: &FragmentEdgeGeometry,
second: &FragmentEdgeGeometry,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
Ok(fragment_edge_lies_on(first, second, tolerance)?
|| fragment_edge_lies_on(second, first, tolerance)?)
}
fn fragment_edge_geometries(
fragment: &FaceFragmentRecord,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
) -> Result<Vec<FragmentEdgeGeometry>, KernelRefusal> {
fragment
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| fragment_edge_geometry(&coedge.source, solids, imprint))
.collect()
}
fn fragment_edge_is_a_point(edge: &FragmentEdgeGeometry, tolerance: f64) -> Result<bool, KernelRefusal> {
let start = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Refine, "evaluate")?;
for fraction in [0.25, 0.5, 0.75, 1.0] {
let point = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
if point.sub(start).length() > tolerance {
return Ok(false);
}
}
Ok(true)
}
pub(super) fn would_overuse_existing_boundary(
candidate: &FaceFragmentRecord,
existing: impl Iterator<Item = FaceFragmentRecord>,
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
let existing_edges = existing
.map(|fragment| fragment_edge_geometries(&fragment, solids, imprint))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.filter(|edge| !fragment_edge_is_a_point(edge, tolerance).unwrap_or(false))
.collect::<Vec<_>>();
for candidate_edge in fragment_edge_geometries(candidate, solids, imprint)? {
if fragment_edge_is_a_point(&candidate_edge, tolerance)? {
continue;
}
let mut matches = 0;
for existing_edge in &existing_edges {
if fragment_edges_overlap(&candidate_edge, existing_edge, tolerance)? {
matches += 1;
if matches >= 2 {
return Ok(true);
}
}
}
}
Ok(false)
}
pub(super) fn fragment_has_sustained_source_contact(
fragment: &FaceFragmentRecord,
source_faces: &[&FaceRecord],
solids: &HashMap<u8, &BrepSolid>,
imprint: &ImprintResultRecord,
tolerance: f64,
) -> Result<bool, KernelRefusal> {
for edge in fragment_edge_geometries(fragment, solids, imprint)? {
for source_face in source_faces {
let mut sustained = true;
for fraction in [0.2, 0.5, 0.8] {
let point = edge
.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let projection = project_point_to_surface(&source_face.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
if projection.distance > tolerance
|| parameter_point_in_face(
source_face,
Vec2 {
x: projection.u,
y: projection.v,
},
tolerance,
).or_refuse(KernelStage::Refine, "parameter_point_in_face")? == PolygonClass::Outside
{
sustained = false;
break;
}
}
if sustained {
return Ok(true);
}
}
}
Ok(false)
}
pub(super) struct CompositeOperands {
pub solid: BrepSolid,
pub faces: HashMap<u64, (u8, u64)>,
pub edges: HashMap<u64, (u8, u64)>,
pub face_index: Vec<usize>,
}
const COMPOSITE_STRIDE: u64 = 1 << 32;
pub(super) fn compose_imprint_operands(members: &[(u8, &BrepSolid)]) -> CompositeOperands {
let mut faces_out = Vec::new();
let mut edges_out = Vec::new();
let mut vertices_out = Vec::new();
let mut faces = HashMap::default();
let mut edges = HashMap::default();
let mut face_index = Vec::with_capacity(members.len());
for (member, (operand, solid)) in members.iter().enumerate() {
let offset = (member as u64 + 1) * COMPOSITE_STRIDE;
for vertex in &solid.vertices {
vertices_out.push(VertexRecord { id: vertex.id + offset, ..vertex.clone() });
}
for edge in &solid.edges {
edges.insert(edge.id + offset, (*operand, edge.id));
edges_out.push(EdgeRecord {
id: edge.id + offset,
start_vertex_id: edge.start_vertex_id + offset,
end_vertex_id: edge.end_vertex_id + offset,
..edge.clone()
});
}
face_index.push(faces_out.len());
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
faces.insert(face.id + offset, (*operand, face.id));
let mut copy = face.clone();
copy.id += offset;
for loop_record in &mut copy.loops {
loop_record.id += offset;
for coedge in &mut loop_record.coedges {
coedge.id += offset;
coedge.edge_id += offset;
}
}
faces_out.push(copy);
}
}
CompositeOperands {
solid: BrepSolid {
mass_properties_cache: Default::default(),
cells: Default::default(),
id: 1,
vertices: vertices_out,
edges: edges_out,
shells: vec![ShellRecord { id: 1, faces: faces_out }],
genus: 0,
kind: crate::BodyKind::Solid,
},
faces,
edges,
face_index,
}
}
pub(super) fn translate_composite_imprint(
record: ImprintResultRecord,
composite: &CompositeOperands,
tolerance: f64,
scale: f64,
) -> Result<ImprintResultRecord, KernelRefusal> {
let face = |key: &FaceKey| -> Result<FaceKey, KernelRefusal> {
let (operand, face_id) = *composite.faces.get(&key.face_id).ok_or_else(|| {
KernelRefusal::internal(KernelStage::Intersect, "offset_shell.composite", format!("composite face {} unknown", key.face_id))
})?;
Ok(FaceKey { operand, face_id })
};
let edge = |operand_edge: u64| -> Result<(u8, u64), KernelRefusal> {
composite.edges.get(&operand_edge).copied().ok_or_else(|| {
KernelRefusal::internal(KernelStage::Intersect, "offset_shell.composite", format!("composite edge {operand_edge} unknown"))
})
};
let mut out = empty_imprint();
out.vertices = record.vertices;
out.tangent_nodes = record.tangent_nodes;
out.section_evidence = record.section_evidence;
out.tangent_sections = record.tangent_sections;
for piece in record.pieces {
let mut pcurves = Vec::with_capacity(piece.pcurves.len());
for pcurve in piece.pcurves {
let key = face(&FaceKey { operand: pcurve.operand, face_id: pcurve.face_id })?;
pcurves.push(FacePcurve { operand: key.operand, face_id: key.face_id, pcurve: pcurve.pcurve });
}
let shared_edge = match piece.shared_edge {
Some((_, edge_id, aligned)) => {
let (operand, original) = edge(edge_id)?;
Some((operand, original, aligned))
}
None => None,
};
out.pieces.push(ImprintPieceRecord {
id: piece.id,
curve: piece.curve,
t0: piece.t0,
t1: piece.t1,
start_vertex_id: piece.start_vertex_id,
end_vertex_id: piece.end_vertex_id,
pcurves,
support_faces: [face(&piece.support_faces[0])?, face(&piece.support_faces[1])?],
shared_edge,
});
}
for by_face in record.by_face {
let key = face(&FaceKey { operand: by_face.operand, face_id: by_face.face_id })?;
let entry = if let Some(entry) = out
.by_face
.iter_mut()
.find(|entry| entry.operand == key.operand && entry.face_id == key.face_id)
{
entry
} else {
out.by_face.push(FaceImprints { operand: key.operand, face_id: key.face_id, piece_ids: Vec::new() });
out.by_face.last_mut().unwrap()
};
for id in by_face.piece_ids {
if !entry.piece_ids.contains(&id) {
entry.piece_ids.push(id);
}
}
}
for split in record.edge_splits {
let (operand, edge_id) = edge(split.edge_id)?;
if let Some(existing) = out
.edge_splits
.iter_mut()
.find(|entry| entry.operand == operand && entry.edge_id == edge_id)
{
for parameter in split.parameters {
if !existing.parameters.iter().any(|value| (*value - parameter).abs() <= 1e-8) {
existing.parameters.push(parameter);
}
}
} else {
out.edge_splits.push(EdgeSplitRecord { operand, edge_id, parameters: split.parameters });
}
}
for (_, edge_id) in record.barrier_edges {
let key = edge(edge_id)?;
if !out.barrier_edges.contains(&key) {
out.barrier_edges.push(key);
}
}
for (first, second) in record.cosurface_pairs {
out.cosurface_pairs.push((face(&first)?, face(&second)?));
}
let merge = tolerance.max(1e-5) * crate::tolerance::merge_scale(scale);
let mut kept: Vec<ImprintVertex> = Vec::with_capacity(out.vertices.len());
let mut remap: HashMap<u64, u64> = HashMap::default();
for vertex in &out.vertices {
match kept.iter().find(|candidate| candidate.point.sub(vertex.point).length() <= merge) {
Some(candidate) => {
remap.insert(vertex.id, candidate.id);
}
None => kept.push(ImprintVertex { id: vertex.id, point: vertex.point }),
}
}
if !remap.is_empty() {
out.vertices = kept;
for piece in &mut out.pieces {
if let Some(id) = remap.get(&piece.start_vertex_id) {
piece.start_vertex_id = *id;
}
if let Some(id) = remap.get(&piece.end_vertex_id) {
piece.end_vertex_id = *id;
}
}
}
Ok(out)
}