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) fn carve_folded_support(
source: &BrepSolid,
face: &FaceRecord,
distance: f64,
) -> 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);
}
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)?;
if fold_image_extent > fold_image_bar {
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;
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,
}))
}
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;
}
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)))
}
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();
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 - target).abs() <= tolerance
&& point.sub(own_foot).dot(point.sub(foot)) < 0.0
{
return Ok(Some((face.id, separation)));
}
}
Ok(None)
}
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)
}