use super::*;
pub(super) fn extend_offset_carriers_past_open_hole_rims(
carriers: &mut [Carrier],
source: &BrepSolid,
source_faces: &[&FaceRecord],
opening_set: &HashSet<u64>,
smooth_pairs: &HashSet<(usize, usize)>,
scale: f64,
) -> Result<HashSet<usize>, String> {
let weld_band = 2e-3f64.max(scale * 5e-5);
let source_edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let mut extended = HashSet::default();
'carrier: for index in 0..carriers.len() {
if !matches!(carriers[index].kind, OffsetFaceRole::Offset) {
continue;
}
if smooth_pairs
.iter()
.any(|(first, second)| *first == index || *second == index)
{
continue;
}
let Some(source_face) = source_faces
.iter()
.find(|face| face.id == carriers[index].source_face_id)
else {
continue;
};
let mut local_uses = HashMap::<u64, usize>::default();
for coedge in source_face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
*local_uses.entry(coedge.edge_id).or_default() += 1;
}
let mut rims = Vec::new();
for (loop_index, loop_record) in source_face.loops.iter().enumerate() {
for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
let Some(edge) = source_edge_by_id.get(&coedge.edge_id) else {
continue 'carrier;
};
if edge.degenerate {
continue;
}
match local_uses[&coedge.edge_id] {
2 => {}
1 if edge.start_vertex_id == edge.end_vertex_id => {
rims.push((loop_index, coedge_index, (*edge).clone()));
}
_ => continue 'carrier,
}
}
}
if rims.len() != 2 {
continue;
}
let carrier_face = carriers[index].solid.shells[0].faces[0].clone();
let surface = carrier_face.surface.clone();
let [su0, su1] = surface.domain_u()?;
let [sv0, sv1] = surface.domain_v()?;
let v_mid = (sv0 + sv1) * 0.5;
if surface
.evaluate(su0, v_mid)?
.sub(surface.evaluate(su1, v_mid)?)
.length()
> weld_band
{
continue;
}
let mut rim_planes = Vec::new();
for (loop_index, coedge_index, rim_edge) in &rims {
let source_coedge = &source_face.loops[*loop_index].coedges[*coedge_index];
let [p0, p1] = source_coedge.pcurve.domain()?;
let mut u_low = f64::MAX;
let mut u_high = f64::MIN;
let mut v_mean = 0.0f64;
for sample in 0..=32 {
let uv = source_coedge
.pcurve
.evaluate(p0 + (p1 - p0) * sample as f64 / 32.0)?;
u_low = u_low.min(uv.x);
u_high = u_high.max(uv.x);
v_mean += uv.y / 33.0;
}
let u_span = su1 - su0;
if (u_low - su0).abs() > u_span * 1e-3 || (u_high - su1).abs() > u_span * 1e-3 {
continue 'carrier;
}
let Some(opening) = source_faces.iter().find(|face| {
opening_set.contains(&face.id)
&& face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == rim_edge.id)
}) else {
continue 'carrier;
};
let Some((plane_point, plane_normal)) =
planar_surface_frame(&opening.surface, weld_band)?
else {
continue 'carrier;
};
let mut rim_loop_area = None;
let mut largest_other = 0.0f64;
for loop_record in &opening.loops {
let area = parameter_space_area(&FaceRecord {
id: 0,
surface: opening.surface.clone(),
same_sense: true,
loops: vec![loop_record.clone()],
name: None,
})?
.abs();
if loop_record
.coedges
.iter()
.any(|coedge| coedge.edge_id == rim_edge.id)
{
rim_loop_area = Some(area);
} else {
largest_other = largest_other.max(area);
}
}
let Some(rim_loop_area) = rim_loop_area else {
continue 'carrier;
};
if rim_loop_area >= largest_other {
continue 'carrier;
}
let image_coedge = &carrier_face.loops[*loop_index].coedges[*coedge_index];
let Some(image_edge) = carriers[index]
.solid
.edges
.iter()
.find(|edge| edge.id == image_coedge.edge_id)
else {
continue 'carrier;
};
let mut off_plane = 0.0f64;
for sample in 0..=32 {
let point = image_edge.curve.evaluate(
image_edge.t0 + (image_edge.t1 - image_edge.t0) * sample as f64 / 32.0,
)?;
off_plane = off_plane.max(point.sub(plane_point).dot(plane_normal).abs());
}
if off_plane <= weld_band {
continue 'carrier;
}
rim_planes.push((v_mean, plane_point, plane_normal));
}
rim_planes.sort_by(|first, second| first.0.total_cmp(&second.0));
let interior = surface.evaluate((su0 + su1) * 0.5, v_mid)?;
for (v_end, (_, plane_point, plane_normal)) in [(sv0, rim_planes[0]), (sv1, rim_planes[1])]
{
let interior_sign = interior.sub(plane_point).dot(plane_normal).signum();
let iso = surface.iso_curve_v(v_end)?;
let [t0, t1] = iso.domain()?;
for sample in 0..=32 {
let point = iso.evaluate(t0 + (t1 - t0) * sample as f64 / 32.0)?;
if point.sub(plane_point).dot(plane_normal) * interior_sign > -weld_band {
continue 'carrier;
}
}
}
let winding = parameter_space_area(&carrier_face)?;
let bottom_rim = surface.iso_curve_v(sv0)?;
let top_rim = surface.iso_curve_v(sv1)?;
let seam = surface.iso_curve_u(su0)?;
let [bottom_t0, bottom_t1] = bottom_rim.domain()?;
let [top_t0, top_t1] = top_rim.domain()?;
let corner_bottom = surface.evaluate(su0, sv0)?;
let corner_top = surface.evaluate(su0, sv1)?;
let flat = |u: f64, v: f64| Vec3::new(u, v, 0.0);
let mut coedges = vec![
CoedgeRecord {
id: 1,
edge_id: 1,
forward: true,
pcurve: crate::make_line(flat(su0, sv0), flat(su1, sv0))?,
},
CoedgeRecord {
id: 2,
edge_id: 3,
forward: true,
pcurve: crate::make_line(flat(su1, sv0), flat(su1, sv1))?,
},
CoedgeRecord {
id: 3,
edge_id: 2,
forward: false,
pcurve: crate::make_line(flat(su1, sv1), flat(su0, sv1))?,
},
CoedgeRecord {
id: 4,
edge_id: 3,
forward: false,
pcurve: crate::make_line(flat(su0, sv1), flat(su0, sv0))?,
},
];
if winding < 0.0 {
coedges.reverse();
for coedge in &mut coedges {
coedge.forward = !coedge.forward;
coedge.pcurve = coedge.pcurve.reversed()?;
}
}
os_debug!(
"carrier[{index}] src={} extended past opening planes: band v=({sv0:.4},{sv1:.4})",
carriers[index].source_face_id,
);
carriers[index].solid = BrepSolid {
id: carriers[index].solid.id,
vertices: vec![
VertexRecord {
id: 1,
point: corner_bottom,
},
VertexRecord {
id: 2,
point: corner_top,
},
],
edges: vec![
EdgeRecord {
id: 1,
curve: bottom_rim,
t0: bottom_t0,
t1: bottom_t1,
start_vertex_id: 1,
end_vertex_id: 1,
degenerate: false,
name: None,
},
EdgeRecord {
id: 2,
curve: top_rim,
t0: top_t0,
t1: top_t1,
start_vertex_id: 2,
end_vertex_id: 2,
degenerate: false,
name: None,
},
EdgeRecord {
id: 3,
curve: seam,
t0: sv0,
t1: sv1,
start_vertex_id: 1,
end_vertex_id: 2,
degenerate: false,
name: None,
},
],
shells: vec![ShellRecord {
id: 1,
faces: vec![FaceRecord {
id: carrier_face.id,
surface,
same_sense: carrier_face.same_sense,
loops: vec![LoopRecord { id: 1, coedges }],
name: carrier_face.name.clone(),
}],
}],
genus: 0,
};
extended.insert(index);
}
Ok(extended)
}
pub(super) fn extend_fallshort_curved_carriers(
carriers: &mut [Carrier],
source: &BrepSolid,
source_faces: &[&FaceRecord],
opening_set: &HashSet<u64>,
smooth_pairs: &HashSet<(usize, usize)>,
already_extended: &HashSet<usize>,
scale: f64,
distance: f64,
) -> Result<usize, String> {
let weld_band = 2e-3f64.max(scale * 5e-5);
let source_edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let mut extended = 0usize;
'carrier: for index in 0..carriers.len() {
if !matches!(carriers[index].kind, OffsetFaceRole::Offset)
|| already_extended.contains(&index)
{
continue;
}
if smooth_pairs
.iter()
.any(|(first, second)| *first == index || *second == index)
{
continue;
}
let Some(source_face) = source_faces
.iter()
.find(|face| face.id == carriers[index].source_face_id)
else {
continue;
};
if source_face.surface.is_affine()? {
continue;
}
let carrier_face = carriers[index].solid.shells[0].faces[0].clone();
let surface = carrier_face.surface.clone();
let [su0, su1] = surface.domain_u()?;
let [sv0, sv1] = surface.domain_v()?;
let v_mid = (sv0 + sv1) * 0.5;
if surface
.evaluate(su0, v_mid)?
.sub(surface.evaluate(su1, v_mid)?)
.length()
> weld_band
{
continue;
}
let mut qualifies = false;
'rims: for (loop_index, loop_record) in source_face.loops.iter().enumerate() {
for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
let Some(edge) = source_edge_by_id.get(&coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
let Some(opening) = source_faces.iter().find(|face| {
opening_set.contains(&face.id)
&& face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|other| other.edge_id == coedge.edge_id)
}) else {
continue;
};
let Some((plane_point, mut plane_normal)) =
planar_surface_frame(&opening.surface, weld_band)?
else {
continue;
};
let Some(outward) = opening
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find_map(|opening_coedge| {
let [p0, p1] = opening_coedge.pcurve.domain().ok()?;
let opening_uv =
opening_coedge.pcurve.evaluate((p0 + p1) * 0.5).ok()?;
face_normal(opening, opening_uv.x, opening_uv.y).ok()
})
else {
continue;
};
if plane_normal.dot(outward) < 0.0 {
plane_normal = plane_normal.scale(-1.0);
}
if edge.start_vertex_id == edge.end_vertex_id {
let mut rim_loop_area = None;
let mut largest_other = 0.0f64;
for loop_record in &opening.loops {
let area = parameter_space_area(&FaceRecord {
id: 0,
surface: opening.surface.clone(),
same_sense: true,
loops: vec![loop_record.clone()],
name: None,
})?
.abs();
if loop_record
.coedges
.iter()
.any(|other| other.edge_id == coedge.edge_id)
{
rim_loop_area = Some(area);
} else {
largest_other = largest_other.max(area);
}
}
let Some(rim_loop_area) = rim_loop_area else {
continue;
};
if rim_loop_area >= largest_other {
continue;
}
}
let Some(image_coedge) = carrier_face
.loops
.get(loop_index)
.and_then(|loop_record| loop_record.coedges.get(coedge_index))
else {
continue 'carrier;
};
let Some(image_edge) = carriers[index]
.solid
.edges
.iter()
.find(|edge| edge.id == image_coedge.edge_id)
else {
continue 'carrier;
};
let mut falls_short = true;
for sample in 0..=16 {
let point = image_edge.curve.evaluate(
image_edge.t0 + (image_edge.t1 - image_edge.t0) * sample as f64 / 16.0,
)?;
if point.sub(plane_point).dot(plane_normal) > -weld_band {
falls_short = false;
break;
}
}
if !falls_short {
continue;
}
let mut crosses = false;
'grid: for iu in 0..=16 {
for iv in 0..=16 {
let point = surface.evaluate(
su0 + (su1 - su0) * iu as f64 / 16.0,
sv0 + (sv1 - sv0) * iv as f64 / 16.0,
)?;
if point.sub(plane_point).dot(plane_normal) > weld_band {
crosses = true;
break 'grid;
}
}
}
if crosses {
qualifies = true;
break 'rims;
}
}
}
if !qualifies {
continue;
}
if rebuild_carrier_full_domain(
&mut carriers[index],
source_face,
weld_band,
distance,
index,
"a fall-short opening rim",
)? {
extended += 1;
}
}
Ok(extended)
}
fn rebuild_carrier_full_domain(
carrier: &mut Carrier,
source_face: &FaceRecord,
weld_band: f64,
distance: f64,
index: usize,
reason: &str,
) -> Result<bool, String> {
let carrier_face = carrier.solid.shells[0].faces[0].clone();
let mut surface = carrier_face.surface.clone();
let [mut su0, mut su1] = surface.domain_u()?;
let [mut sv0, mut sv1] = surface.domain_v()?;
let v_mid = (sv0 + sv1) * 0.5;
if surface
.evaluate(su0, v_mid)?
.sub(surface.evaluate(su1, v_mid)?)
.length()
> weld_band
{
return Ok(false);
}
if let Some(crate::AnalyticSurface::Sphere { frame, radius }) =
crate::analytic_surface::recognize(&source_face.surface)
{
let mid = surface.evaluate((su0 + su1) * 0.5, (sv0 + sv1) * 0.5)?;
let measured = mid.sub(frame.origin).length();
let grown = radius + distance.abs();
let shrunk = (radius - distance.abs()).abs();
let target = if (measured - grown).abs() <= (measured - shrunk).abs() {
grown
} else {
shrunk
};
if (measured - target).abs() <= weld_band.max(distance.abs() * 0.5) && target > weld_band {
let full = crate::make_sphere_surface(frame.origin, target, frame.axis)?;
let [fu0, fu1] = full.domain_u()?;
let [fv0, fv1] = full.domain_v()?;
let old_normal = surface.normal((su0 + su1) * 0.5, (sv0 + sv1) * 0.5)?;
let radial = mid.sub(frame.origin).normalized()?;
let sample = full.evaluate((fu0 + fu1) * 0.5, (fv0 + fv1) * 0.5)?;
let new_normal = full.normal((fu0 + fu1) * 0.5, (fv0 + fv1) * 0.5)?;
let new_radial = sample.sub(frame.origin).normalized()?;
if (old_normal.dot(radial) > 0.0) == (new_normal.dot(new_radial) > 0.0) {
os_debug!("carrier[{index}] rebuilt on the exact full offset sphere r={target:.6}");
surface = full;
(su0, su1, sv0, sv1) = (fu0, fu1, fv0, fv1);
} else {
os_debug!("carrier[{index}] sphere rebuild skipped: orientation mismatch");
}
} else {
os_debug!(
"carrier[{index}] sphere rebuild skipped: measured radius {measured:.6} \
matches neither grown nor shrunk offset of {radius:.6}"
);
}
}
let winding = parameter_space_area(&carrier_face)?;
let pole_point = |iso: &crate::NurbsCurve| -> Result<Option<Vec3>, String> {
let [t0, t1] = iso.domain()?;
let anchor = iso.evaluate(t0)?;
let mut deviation = 0.0f64;
for sample in 1..=8 {
let point = iso.evaluate(t0 + (t1 - t0) * sample as f64 / 8.0)?;
deviation = deviation.max(point.sub(anchor).length());
}
os_debug!(" pole probe: max deviation {deviation:.6}");
Ok((deviation <= weld_band).then_some(anchor))
};
let bottom_iso = surface.iso_curve_v(sv0)?;
let top_iso = surface.iso_curve_v(sv1)?;
let bottom_pole = pole_point(&bottom_iso)?;
let top_pole = pole_point(&top_iso)?;
let corner_bottom = surface.evaluate(su0, sv0)?;
let corner_top = surface.evaluate(su0, sv1)?;
let seam = surface.iso_curve_u(su0)?;
let v_end_edge = |id: u64,
iso: crate::NurbsCurve,
pole: Option<Vec3>,
vertex: u64|
-> Result<EdgeRecord, String> {
Ok(match pole {
Some(point) => EdgeRecord {
id,
curve: crate::make_line(point, point)?,
t0: 0.0,
t1: 1.0,
start_vertex_id: vertex,
end_vertex_id: vertex,
degenerate: true,
name: None,
},
None => {
let [t0, t1] = iso.domain()?;
EdgeRecord {
id,
curve: iso,
t0,
t1,
start_vertex_id: vertex,
end_vertex_id: vertex,
degenerate: false,
name: None,
}
}
})
};
let flat = |u: f64, v: f64| Vec3::new(u, v, 0.0);
let mut coedges = vec![
CoedgeRecord {
id: 1,
edge_id: 1,
forward: true,
pcurve: crate::make_line(flat(su0, sv0), flat(su1, sv0))?,
},
CoedgeRecord {
id: 2,
edge_id: 3,
forward: true,
pcurve: crate::make_line(flat(su1, sv0), flat(su1, sv1))?,
},
CoedgeRecord {
id: 3,
edge_id: 2,
forward: false,
pcurve: crate::make_line(flat(su1, sv1), flat(su0, sv1))?,
},
CoedgeRecord {
id: 4,
edge_id: 3,
forward: false,
pcurve: crate::make_line(flat(su0, sv1), flat(su0, sv0))?,
},
];
if winding < 0.0 {
coedges.reverse();
for coedge in &mut coedges {
coedge.forward = !coedge.forward;
coedge.pcurve = coedge.pcurve.reversed()?;
}
}
os_debug!(
"carrier[{index}] src={} extended to full domain past {reason} \
(poles: bottom={} top={})",
carrier.source_face_id,
bottom_pole.is_some(),
top_pole.is_some(),
);
carrier.solid = BrepSolid {
id: carrier.solid.id,
vertices: vec![
VertexRecord {
id: 1,
point: corner_bottom,
},
VertexRecord {
id: 2,
point: corner_top,
},
],
edges: vec![
v_end_edge(1, bottom_iso, bottom_pole, 1)?,
v_end_edge(2, top_iso, top_pole, 2)?,
EdgeRecord {
id: 3,
curve: seam,
t0: sv0,
t1: sv1,
start_vertex_id: 1,
end_vertex_id: 2,
degenerate: false,
name: None,
},
],
shells: vec![ShellRecord {
id: 1,
faces: vec![FaceRecord {
id: carrier_face.id,
surface,
same_sense: carrier_face.same_sense,
loops: vec![LoopRecord { id: 1, coedges }],
name: carrier_face.name.clone(),
}],
}],
genus: 0,
};
Ok(true)
}
pub(super) fn rebuild_reflex_rim_carriers(
carriers: &mut [Carrier],
source: &BrepSolid,
source_faces: &[&FaceRecord],
smooth_pairs: &HashSet<(usize, usize)>,
scale: f64,
distance: f64,
) -> Result<HashSet<usize>, String> {
let weld_band = 2e-3f64.max(scale * 5e-5);
let mut rebuilt = HashSet::default();
for index in 0..carriers.len() {
if !matches!(carriers[index].kind, OffsetFaceRole::Offset) {
continue;
}
if smooth_pairs
.iter()
.any(|(first, second)| *first == index || *second == index)
{
continue;
}
let Some(source_face) = source_faces
.iter()
.find(|face| face.id == carriers[index].source_face_id)
else {
continue;
};
if source_face.surface.is_affine()? {
continue;
}
if face_reflex_miter_tan(source, source_face)?.is_none() {
continue;
}
if rebuild_carrier_full_domain(
&mut carriers[index],
source_face,
weld_band,
distance,
index,
"a reflex junction rim",
)? {
rebuilt.insert(index);
}
}
Ok(rebuilt)
}
pub(super) struct JunctionStation {
pub normal: Vec3,
pub mate_normal: Vec3,
pub tangent: Vec3,
}
impl JunctionStation {
pub fn is_convex(&self) -> bool {
self.normal.cross(self.mate_normal).dot(self.tangent) > 0.0
}
pub fn miter_tan(&self) -> f64 {
let cos_normals = self.normal.dot(self.mate_normal).clamp(-1.0, 1.0);
(cos_normals.acos() * 0.5).tan()
}
}
pub(super) fn junction_stations(
face: &FaceRecord,
coedge: &CoedgeRecord,
edge: &EdgeRecord,
mate: &FaceRecord,
) -> Result<Vec<JunctionStation>, String> {
let mut stations = Vec::new();
for fraction in [0.1f64, 0.3, 0.5, 0.7, 0.9] {
let t_sample = edge.t0 + (edge.t1 - edge.t0) * fraction;
let step = ((edge.t1 - edge.t0) * 1e-3).max(1e-9);
let before = edge.curve.evaluate(t_sample - step)?;
let after = edge.curve.evaluate(t_sample + step)?;
let mut tangent = after.sub(before);
if tangent.length() <= 1e-12 {
continue;
}
if !coedge.forward {
tangent = tangent.scale(-1.0);
}
let station = edge.curve.evaluate(t_sample)?;
let uv_this = {
let projection = project_point_to_surface(&face.surface, station)?;
Vec2 {
x: projection.u,
y: projection.v,
}
};
let uv_mate = {
let projection = project_point_to_surface(&mate.surface, station)?;
Vec2 {
x: projection.u,
y: projection.v,
}
};
let normal = face_normal(face, uv_this.x, uv_this.y)?;
let mate_normal = face_normal(mate, uv_mate.x, uv_mate.y)?;
let cross = normal.cross(mate_normal);
if cross.length() <= 1e-6 || normal.dot(mate_normal) >= SMOOTH_JUNCTION_COS {
continue;
}
stations.push(JunctionStation {
normal,
mate_normal,
tangent,
});
}
Ok(stations)
}
pub(super) fn junction_mate<'a>(
faces: &[&'a FaceRecord],
face: &FaceRecord,
edge_id: u64,
) -> Option<(&'a FaceRecord, &'a CoedgeRecord)> {
faces.iter().find_map(|other| {
if other.id == face.id {
return None;
}
other
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|other_coedge| other_coedge.edge_id == edge_id)
.map(|other_coedge| (*other, other_coedge))
})
}
pub(super) fn face_reflex_miter_tan(source: &BrepSolid, face: &FaceRecord) -> Result<Option<f64>, String> {
let edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let faces = source
.shells
.iter()
.flat_map(|shell| &shell.faces)
.collect::<Vec<_>>();
let mut worst: Option<f64> = None;
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
let Some((mate, _)) = junction_mate(&faces, face, coedge.edge_id) else {
continue;
};
for station in junction_stations(face, coedge, edge, mate)? {
if !station.is_convex() {
worst = Some(worst.unwrap_or(0.0).max(station.miter_tan()));
}
}
}
}
Ok(worst)
}
pub(super) fn outward_miter_scale(worst_tan: Option<f64>) -> f64 {
match worst_tan {
Some(tan) if tan > 1.0 + 1e-6 => (tan * 1.5).min(12.0),
_ => 1.0,
}
}
pub(super) fn outward_carrier_extension(
source: &BrepSolid,
face: &FaceRecord,
source_faces: &[&FaceRecord],
opening_set: &HashSet<u64>,
amount: f64,
) -> Result<crate::CarrierExtension, String> {
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let mut uses: Vec<(Vec<Vec2>, bool, Option<f64>)> = Vec::new();
let mut low = Vec2 {
x: f64::INFINITY,
y: f64::INFINITY,
};
let mut high = Vec2 {
x: f64::NEG_INFINITY,
y: f64::NEG_INFINITY,
};
for coedge in face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
if edge_by_id
.get(&coedge.edge_id)
.is_some_and(|edge| edge.degenerate)
{
continue;
}
let [p0, p1] = coedge.pcurve.domain()?;
let samples = (0..=24)
.map(|sample| {
coedge
.pcurve
.evaluate(p0 + (p1 - p0) * sample as f64 / 24.0)
.map(|uv| Vec2 { x: uv.x, y: uv.y })
})
.collect::<Result<Vec<_>, String>>()?;
for uv in &samples {
low.x = low.x.min(uv.x);
low.y = low.y.min(uv.y);
high.x = high.x.max(uv.x);
high.y = high.y.max(uv.y);
}
let mate = junction_mate(source_faces, face, coedge.edge_id)
.filter(|(mate, _)| !opening_set.contains(&mate.id));
let (smooth, convex_tan) = match mate {
Some((mate, mate_use)) => {
let smooth = uses_are_tangent(face, coedge, mate, mate_use)?;
let convex_tan = match edge_by_id.get(&coedge.edge_id) {
Some(edge) if !smooth => junction_stations(face, coedge, edge, mate)?
.iter()
.filter(|station| station.is_convex())
.map(JunctionStation::miter_tan)
.fold(None, |worst: Option<f64>, tan| {
Some(worst.unwrap_or(0.0).max(tan))
}),
_ => None,
};
(smooth, convex_tan)
}
None => (false, None),
};
uses.push((samples, smooth, convex_tan));
}
if uses.is_empty() || !low.x.is_finite() || !high.x.is_finite() {
return Ok(crate::CarrierExtension::uniform(amount));
}
let worst_of = |first: Option<f64>, tan: Option<f64>| -> Option<f64> {
match (first, tan) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, b) => a.or(b),
}
};
let face_worst = uses.iter().fold(None, |worst, (_, _, convex_tan)| {
worst_of(worst, *convex_tan)
});
let side = |axis: fn(&Vec2) -> f64, value: f64, span: f64| -> f64 {
let band = (span.abs() * 1e-5).max(1e-9);
let mut along = 0usize;
let mut smooth_along = 0usize;
let mut worst_along: Option<f64> = None;
for (samples, smooth, convex_tan) in &uses {
let on = samples
.iter()
.filter(|uv| (axis(uv) - value).abs() <= band)
.count();
if on * 10 >= samples.len() * 9 {
along += 1;
if *smooth {
smooth_along += 1;
}
worst_along = worst_of(worst_along, *convex_tan);
}
}
if along > 0 && along == smooth_along {
return 0.0;
}
let worst = if along > 0 { worst_along } else { face_worst };
amount * outward_miter_scale(worst)
};
Ok(crate::CarrierExtension {
u_min: side(|uv| uv.x, low.x, u1 - u0),
u_max: side(|uv| uv.x, high.x, u1 - u0),
v_min: side(|uv| uv.y, low.y, v1 - v0),
v_max: side(|uv| uv.y, high.y, v1 - v0),
})
}
pub(super) fn outward_wall_pad(
source: &BrepSolid,
opening: &FaceRecord,
source_faces: &[&FaceRecord],
opening_set: &HashSet<u64>,
amount: f64,
) -> Result<f64, String> {
let edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let mut worst: Option<f64> = None;
for coedge in opening
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
let Some((mate, _)) = junction_mate(source_faces, opening, coedge.edge_id)
.filter(|(mate, _)| !opening_set.contains(&mate.id))
else {
continue;
};
for station in junction_stations(opening, coedge, edge, mate)? {
if !station.is_convex() {
continue;
}
let sin_dihedral = station.normal.cross(station.mate_normal).length();
if sin_dihedral <= 1e-6 {
continue;
}
worst = Some(worst.unwrap_or(0.0).max(1.0 / sin_dihedral));
}
}
Ok(amount * outward_miter_scale(worst))
}
pub(super) fn ruled_opening(face: &FaceRecord) -> Result<bool, String> {
let surface = &face.surface;
Ok(face.loops.len() == 1
&& !surface.is_affine()?
&& surface.degree_v == 1
&& surface.control_points.len() >= 3
&& surface.control_points.iter().all(|row| row.len() == 2))
}
pub(super) fn extended_ruled_wall(
source: &BrepSolid,
face: &FaceRecord,
pad: f64,
tolerance: f64,
) -> Result<Result<RuledWall, String>, String> {
use crate::{NurbsSurface, SurfaceSide};
let surface = &face.surface;
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let loops = face
.loops
.iter()
.map(|record| {
record
.coedges
.iter()
.map(|coedge| coedge.pcurve.clone())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
if !crate::thicken::band::whole_face(&loops, [u0, u1], [v0, v1])? {
return Ok(Err("the opening's trim is not the whole face".into()));
}
let mut wall = standalone_face(source, face.id)?;
if wall.edges.iter().any(|edge| edge.degenerate) {
return Ok(Err("the opening has a degenerate edge".into()));
}
let (closed_u, closed_v) = surface.closed_directions()?;
const STATIONS: usize = 8;
let side_growth = |grown: &NurbsSurface, side: SurfaceSide, delta: f64| -> Result<f64, String> {
let mut least = f64::INFINITY;
for index in 0..=STATIONS {
let fraction = index as f64 / STATIONS as f64;
let (old, new) = match side {
SurfaceSide::UMin | SurfaceSide::UMax => {
let v = v0 + (v1 - v0) * fraction;
let u = if matches!(side, SurfaceSide::UMin) { u0 } else { u1 };
let step = if matches!(side, SurfaceSide::UMin) { -delta } else { delta };
((u, v), (u + step, v))
}
SurfaceSide::VMin | SurfaceSide::VMax => {
let u = u0 + (u1 - u0) * fraction;
let v = if matches!(side, SurfaceSide::VMin) { v0 } else { v1 };
let step = if matches!(side, SurfaceSide::VMin) { -delta } else { delta };
((u, v), (u, v + step))
}
};
let from = grown.evaluate(old.0, old.1)?;
let to = grown.evaluate(new.0, new.1)?;
least = least.min(to.sub(from).length());
}
Ok(least)
};
let mut grown = surface.clone();
let mut deltas = [0.0f64; 4];
let mut reached = [0.0f64; 4];
for (slot, side) in [
SurfaceSide::UMin,
SurfaceSide::UMax,
SurfaceSide::VMin,
SurfaceSide::VMax,
]
.into_iter()
.enumerate()
{
if (side.is_u() && closed_u) || (!side.is_u() && closed_v) {
continue;
}
let mut speed = f64::INFINITY;
for index in 0..=STATIONS {
let fraction = index as f64 / STATIONS as f64;
let (u, v) = match side {
SurfaceSide::UMin => (u0, v0 + (v1 - v0) * fraction),
SurfaceSide::UMax => (u1, v0 + (v1 - v0) * fraction),
SurfaceSide::VMin => (u0 + (u1 - u0) * fraction, v0),
SurfaceSide::VMax => (u0 + (u1 - u0) * fraction, v1),
};
let derivatives = surface.derivatives(u, v, 1)?;
let along = if side.is_u() { derivatives[1][0] } else { derivatives[0][1] };
speed = speed.min(along.length());
}
if !(speed > 0.0) {
return Ok(Err(format!("the opening's {side:?} side has no speed to grow along")));
}
let mut delta = pad / speed;
let mut reach = pad;
let mut attempt = None;
for _ in 0..8 {
let candidate = match grown.extend_natural(side, delta) {
Ok(candidate) => candidate,
Err(crate::ExtendRefusal::ExcessiveGrowth { ratio, limit }) if ratio > limit => {
let shrink = limit / ratio * 0.95;
reach *= shrink;
delta *= shrink;
continue;
}
Err(refusal) => {
return Ok(Err(format!(
"the opening cannot be continued past its {side:?} side by {reach:.6}: {refusal}"
)));
}
};
let growth = side_growth(&candidate, side, delta)?;
if growth >= reach * (1.0 - 1e-9) {
attempt = Some((candidate, growth));
break;
}
delta *= (reach / growth.max(reach * 1e-3)) * 1.05;
}
let Some((candidate, growth)) = attempt else {
return Ok(Err(format!(
"the opening's {side:?} side does not reach {reach:.6} along its continuation"
)));
};
reached[slot] = growth;
grown = candidate;
deltas[slot] = delta;
}
let remap = |knots: &[f64], low: f64, high: f64, start: f64, end: f64| -> Vec<f64> {
knots
.iter()
.map(|knot| start + (knot - low) * (end - start) / (high - low))
.collect()
};
let surface = NurbsSurface::new(
grown.degree_u,
grown.degree_v,
remap(&grown.knots_u, u0 - deltas[0], u1 + deltas[1], u0, u1),
remap(&grown.knots_v, v0 - deltas[2], v1 + deltas[3], v0, v1),
grown.control_points.clone(),
)?;
let wall_face = &mut wall.shells[0].faces[0];
wall_face.surface = surface.clone();
let uses = wall_face
.loops
.iter()
.flat_map(|record| &record.coedges)
.cloned()
.collect::<Vec<_>>();
for edge in &mut wall.edges {
let Some(coedge) = uses.iter().find(|coedge| coedge.edge_id == edge.id) else {
continue;
};
let image = crate::image_curve(&surface, &coedge.pcurve, tolerance, "offset_shell ruled wall")?;
let (mut first, mut last) = (image.t0, image.t1);
if !coedge.forward {
std::mem::swap(&mut first, &mut last);
}
let mut curve = image.curve;
if first > last {
let [start, end] = curve.domain()?;
curve = curve.reversed()?;
first = start + end - first;
last = start + end - last;
}
let (start_point, end_point) = (curve.evaluate(first)?, curve.evaluate(last)?);
for vertex in &mut wall.vertices {
if vertex.id == edge.start_vertex_id {
vertex.point = start_point;
} else if vertex.id == edge.end_vertex_id {
vertex.point = end_point;
}
}
edge.curve = curve;
edge.t0 = first;
edge.t1 = last;
}
Ok(Ok(RuledWall { solid: wall, reached }))
}
pub(super) struct RuledWall {
pub solid: BrepSolid,
pub reached: [f64; 4],
}
pub(super) fn outward_fold_the_carve_missed(
face: &FaceRecord,
distance: f64,
) -> Result<Option<crate::offset_regularity::FoldSample>, String> {
use crate::offset_regularity::{fold_sample_at, scan_offset_regularity, TrimRegion};
if distance >= 0.0 || face.surface.is_affine()? {
return Ok(None);
}
let region = TrimRegion::from_face(face)?;
let displacement = [shell_displacement(face, distance)];
let scan = scan_offset_regularity(
&face.surface,
®ion,
&displacement,
COLLAPSE_FACTOR,
UNSEEN_FOLD_BUDGET,
)?;
let Some(mut worst) = scan.worst else {
return Ok(None);
};
let [u_low, u_high, v_low, v_high] = region.bounds();
let mut radius = [(u_high - u_low) / 64.0, (v_high - v_low) / 64.0];
let ratio = 0.5 * (5f64.sqrt() - 1.0);
for _ in 0..6 {
for along_u in [true, false] {
let (centre, low, high, reach) = if along_u {
(worst.u, u_low, u_high, radius[0])
} else {
(worst.v, v_low, v_high, radius[1])
};
let sample = |t: f64| {
let (u, v) = if along_u { (t, worst.v) } else { (worst.u, t) };
region
.contains(u, v)
.then(|| fold_sample_at(&face.surface, u, v, &displacement))
.flatten()
};
let (mut a, mut b) = ((centre - reach).max(low), (centre + reach).min(high));
for _ in 0..48 {
let (c, d) = (b - ratio * (b - a), a + ratio * (b - a));
match (sample(c), sample(d)) {
(Some(first), Some(second)) if first.factor < second.factor => b = d,
(Some(_), Some(_)) => a = c,
_ => break,
}
}
if let Some(polished) = sample(0.5 * (a + b)) {
if polished.factor < worst.factor {
worst = polished;
}
}
}
radius = [radius[0] * 0.5, radius[1] * 0.5];
}
Ok((worst.factor <= COLLAPSE_FACTOR).then_some(worst))
}
const UNSEEN_FOLD_BUDGET: crate::offset_regularity::ScanBudget =
crate::offset_regularity::ScanBudget {
per_span: 9,
steps_per_distance: 2.0,
max_nodes: 4096,
refine: true,
};
pub(super) fn fallshort_opening_reach(
source: &BrepSolid,
face: &FaceRecord,
source_faces: &[&FaceRecord],
opening_set: &HashSet<u64>,
distance: f64,
) -> Result<Option<(bool, f64)>, String> {
if distance <= 0.0 || face.surface.is_affine()? {
return Ok(None);
}
let surface = &face.surface;
if surface.degree_v != 1 || surface.control_points.iter().any(|row| row.len() != 2) {
return Ok(None);
}
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let offsets = face_offsets(face);
let mut best: Option<(bool, f64)> = None;
for coedge in face.loops.iter().flat_map(|record| &record.coedges) {
let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
let Some((opening, _)) = junction_mate(source_faces, face, coedge.edge_id)
.filter(|(mate, _)| opening_set.contains(&mate.id))
else {
continue;
};
if !surface_is_planar(&opening.surface, 1e-6 * crate::solid_scale(source).max(1.0))? {
continue;
}
let [ou0, ou1] = opening.surface.domain_u()?;
let [ov0, ov1] = opening.surface.domain_v()?;
let (ou, ov) = ((ou0 + ou1) * 0.5, (ov0 + ov1) * 0.5);
let plane_point = opening.surface.evaluate(ou, ov)?;
let Ok(plane_normal) = face_normal(opening, ou, ov) else {
continue;
};
let [p0, p1] = coedge.pcurve.domain()?;
for index in 0..=8 {
let uv = coedge.pcurve.evaluate(p0 + (p1 - p0) * index as f64 / 8.0)?;
let at_v_max = (uv.y - v1).abs() < (uv.y - v0).abs();
let u = uv.x.clamp(u0, u1);
let (near, far) = if at_v_max { (v0, v1) } else { (v1, v0) };
let image = |v: f64| offsets.at(u, v, -distance).map(|sample| sample.point).ok();
let (Some(from), Some(to)) = (image(near), image(far)) else {
continue;
};
let ruling = to.sub(from);
let length = ruling.length();
if length <= 1e-12 {
continue;
}
let direction = ruling.scale(1.0 / length);
let denominator = direction.dot(plane_normal);
if denominator.abs() <= 1e-9 {
continue;
}
let reach = plane_point.sub(to).dot(plane_normal) / denominator;
if reach <= 0.0 {
continue;
}
if best.is_none_or(|(_, worst)| reach > worst) {
best = Some((at_v_max, reach));
}
}
}
Ok(best)
}
pub(super) fn offset_falls_short_of_curved_opening(
source: &BrepSolid,
face: &FaceRecord,
source_faces: &[&FaceRecord],
opening_set: &HashSet<u64>,
distance: f64,
) -> Result<Option<(u64, f64)>, String> {
if distance <= 0.0 {
return Ok(None);
}
let band = 2e-3f64.max(crate::solid_scale(source) * 5e-5);
let edge_by_id = source
.edges
.iter()
.map(|edge| (edge.id, edge))
.collect::<HashMap<_, _>>();
let offsets = face_offsets(face);
let mut worst: Option<(u64, f64)> = None;
for coedge in face.loops.iter().flat_map(|record| &record.coedges) {
let Some(edge) = edge_by_id.get(&coedge.edge_id) else {
continue;
};
if edge.degenerate {
continue;
}
let Some((opening, _)) = junction_mate(source_faces, face, coedge.edge_id)
.filter(|(mate, _)| opening_set.contains(&mate.id))
else {
continue;
};
if surface_is_planar(&opening.surface, band)? {
continue;
}
let [p0, p1] = coedge.pcurve.domain()?;
for index in 0..=8 {
let uv = coedge.pcurve.evaluate(p0 + (p1 - p0) * index as f64 / 8.0)?;
let Ok(image) = offsets.at(uv.x, uv.y, -distance) else {
continue;
};
let gap = project_point_to_surface(&opening.surface, image.point)?.distance;
if gap > band && worst.is_none_or(|(_, seen)| gap > seen) {
worst = Some((opening.id, gap));
}
}
}
Ok(worst)
}
pub(super) fn source_by_id_lookup<'a>(
source_faces: &[&'a FaceRecord],
face_id: u64,
) -> Option<&'a FaceRecord> {
source_faces.iter().find(|face| face.id == face_id).copied()
}
pub(super) fn sample_separation(first: &[Vec3], second: &[Vec3]) -> f64 {
first
.iter()
.flat_map(|a| second.iter().map(move |b| a.sub(*b).length()))
.fold(f64::INFINITY, f64::min)
}
pub(super) fn opening_wall_seeds(
opening: &FaceRecord,
carrier: &FaceRecord,
source: &BrepSolid,
opening_set: &HashSet<u64>,
distance: f64,
) -> Result<Vec<Vec2>, String> {
let source_faces = source
.shells
.iter()
.flat_map(|shell| &shell.faces)
.collect::<Vec<_>>();
let mut seeds = Vec::new();
for opening_use in opening
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
let Some((neighbor, neighbor_use)) = source_faces.iter().find_map(|face| {
if face.id == opening.id || opening_set.contains(&face.id) {
return None;
}
face.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == opening_use.edge_id)
.map(|coedge| (*face, coedge))
}) else {
continue;
};
let [opening_start, opening_end] = opening_use.pcurve.domain()?;
let opening_uv = opening_use
.pcurve
.evaluate((opening_start + opening_end) * 0.5)?;
let original = opening.surface.evaluate(opening_uv.x, opening_uv.y)?;
let [neighbor_start, neighbor_end] = neighbor_use.pcurve.domain()?;
let neighbor_uv = neighbor_use
.pcurve
.evaluate((neighbor_start + neighbor_end) * 0.5)?;
let expected = original.add(
face_offsets(neighbor)
.normal(neighbor_uv.x, neighbor_uv.y)?
.scale(-distance),
);
let halfway = original.add(expected.sub(original).scale(0.5));
let projection = project_point_to_surface(&carrier.surface, halfway)?;
seeds.push(Vec2 {
x: projection.u,
y: projection.v,
});
}
Ok(seeds)
}