use super::*;
fn offset_ruled_carrier(
surface: &NurbsSurface,
signed_distance: f64,
tolerance: f64,
) -> Result<NurbsSurface, String> {
let Some(AnalyticSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
}) = surface.analytic()
else {
return Err("offset_ruled_carrier: face carrier is not a ruled revolution".into());
};
let (frame, rho0, rho1, height) = (frame.clone(), *rho0, *rho1, *height);
let slope = (rho1 - rho0) / height;
let grow = signed_distance * (1.0 + slope * slope).sqrt();
let rho0_new = rho0 + grow;
let rho1_new = rho1 + grow;
if rho0_new <= tolerance || rho1_new <= tolerance {
return Err(
"offset (push): the ruled carrier collapses to or past its axis — refusing".into(),
);
}
let start = frame.origin.add(frame.x_axis.scale(rho0_new));
let end = frame
.origin
.add(frame.axis.scale(height))
.add(frame.x_axis.scale(rho1_new));
let generatrix = make_line(start, end)?;
make_revolution(frame.origin, frame.axis, &generatrix, std::f64::consts::TAU)
}
fn outward_normal_mid(face: &FaceRecord) -> Result<(Vec3, Vec3), String> {
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
let point = face.surface.evaluate(um, vm)?;
let mut normal = face.surface.normal(um, vm)?;
if !face.same_sense {
normal = normal.scale(-1.0);
}
Ok((point, normal))
}
pub fn offset_ruled_face(
solid: &BrepSolid,
face_id: u64,
distance: f64,
) -> Result<BrepSolid, String> {
if !distance.is_finite() {
return Err("offset_ruled_face: distance must be finite".into());
}
let scale = solid_model_scale(solid);
let tolerance = (scale * 1e-7).max(1e-9);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let (pshell, pface) =
find_face(solid, face_id).ok_or_else(|| format!("offset_ruled_face: no face {face_id}"))?;
let pushed = &solid.shells[pshell].faces[pface];
let Some(AnalyticSurface::RuledRevolution { frame, .. }) = pushed.surface.analytic() else {
return Err("offset_ruled_face: the pushed face is not a cylinder or cone".into());
};
let (frame_origin, frame_axis) = (frame.origin, frame.axis);
let (mid_point, mid_normal) = outward_normal_mid(pushed)?;
let radial = {
let d = mid_point.sub(frame_origin);
d.sub(frame_axis.scale(d.dot(frame_axis)))
};
if radial.length() <= tolerance {
return Err("offset_ruled_face: degenerate radial direction (face on the axis)".into());
}
let outward_sign = if mid_normal.dot(radial) >= 0.0 { 1.0 } else { -1.0 };
let signed_distance = distance * outward_sign;
let s_prime = offset_ruled_carrier(&pushed.surface, signed_distance, tolerance)?;
let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
for shell in &solid.shells {
for face in &shell.faces {
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
faces_of_edge
.entry(coedge.edge_id)
.or_default()
.push(face.id);
}
}
}
}
let edge_by_id: HashMap<u64, &EdgeRecord> =
solid.edges.iter().map(|edge| (edge.id, edge)).collect();
let mut new_curve: HashMap<u64, NurbsCurve> = HashMap::default();
let mut new_vertex: HashMap<u64, Vec3> = HashMap::default();
let mut cap_faces: HashSet<u64> = HashSet::default();
let mut ruled_faces: HashSet<u64> = HashSet::default();
let mut curved_faces: HashSet<u64> = HashSet::default();
let mut seam_edges: Vec<u64> = Vec::new();
let vertex_pos: HashMap<u64, Vec3> =
solid.vertices.iter().map(|v| (v.id, v.point)).collect();
let mut open_rims: Vec<OpenRim> = Vec::new();
let mut rim_edges: Vec<u64> = Vec::new();
let mut hole_edges: HashSet<u64> = HashSet::default();
for loop_record in &pushed.loops {
let Some(hole) = single_neighbour_hole(
loop_record,
face_id,
solid,
&faces_of_edge,
&edge_by_id,
frame_origin,
frame_axis,
scale,
)?
else {
continue;
};
rebuild_single_neighbour_hole(
solid,
&s_prime,
&hole,
&edge_by_id,
&vertex_pos,
tolerance,
scale,
&mut new_curve,
&mut new_vertex,
)?;
for edge_id in &hole.edge_ids {
hole_edges.insert(*edge_id);
if !rim_edges.contains(edge_id) {
rim_edges.push(*edge_id);
}
}
curved_faces.insert(hole.neighbour);
}
for loop_record in &pushed.loops {
let coedge_count = loop_record.coedges.len();
for coedge_index in 0..coedge_count {
let coedge = &loop_record.coedges[coedge_index];
let edge = *edge_by_id
.get(&coedge.edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {}", coedge.edge_id))?;
if hole_edges.contains(&edge.id) {
continue; }
let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
let is_seam = incident.iter().all(|f| *f == face_id);
if is_seam {
if !seam_edges.contains(&edge.id) {
seam_edges.push(edge.id);
}
continue;
}
let neighbour = *incident
.iter()
.find(|f| **f != face_id)
.ok_or_else(|| format!("offset_ruled_face: edge {} has no neighbour", edge.id))?;
let (nshell, nface) = find_face(solid, neighbour)
.ok_or_else(|| format!("offset_ruled_face: missing neighbour {neighbour}"))?;
let neighbour_surface = &solid.shells[nshell].faces[nface].surface;
let is_plane =
matches!(neighbour_surface.analytic(), Some(AnalyticSurface::Plane { .. }));
let is_coaxial_ruled =
ruled_neighbour_is_coaxial(neighbour_surface, frame_origin, frame_axis, scale);
let is_curved = !is_plane && !is_coaxial_ruled;
let separated = || {
"offset_ruled_face: the pushed carrier no longer meets a neighbour \
(the push separated them, or the rim left the neighbour's domain) — refusing"
.to_string()
};
let old_mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1))?;
let mut marched = false;
let curves = if is_curved {
if edge.start_vertex_id != edge.end_vertex_id {
return Err(format!(
"offset_ruled_face: the rim against curved neighbour {neighbour} is an \
OPEN arc (edge {}); an arc's corner is a triple point solved against a \
PLANE only — deferred (refusing)",
edge.id
));
}
let policy = MarchPolicy {
tolerance,
residual_tolerance: (scale * 5e-4).max(5e-6),
seeds: edge_seeds(edge, 9)?,
};
match reintersect_carriers(&s_prime, neighbour_surface, &policy) {
Ok(found) => {
marched = found.lane == RimLane::Marched;
if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
eprintln!(
"RIM neighbour {neighbour}: lane {:?}, {} branch(es), \
residual {:.3e} (gate {:.3e})",
found.lane,
found.sections.len(),
found.residual,
policy.residual_tolerance
);
}
found.curves()
}
Err(ReintersectRefusal::Separated) => return Err(separated()),
Err(other) => {
return Err(format!("offset_ruled_face: {}", other.describe()));
}
}
} else {
intersect_analytic_pair(&s_prime, neighbour_surface, tolerance)
.filter(|curves| !curves.is_empty())
.ok_or_else(separated)?
};
let rim = nearest_curve(&curves, old_mid)?;
if !rim_edges.contains(&edge.id) {
rim_edges.push(edge.id);
}
if edge.start_vertex_id == edge.end_vertex_id {
let rim = if marched {
match_marched_rim_direction(rim, edge)?
} else {
match_closed_rim_direction(rim, edge)?
};
let seam_point = if marched {
let [d0, _] = rim.domain()?;
rim.evaluate(d0)?
} else {
rim.evaluate(0.0)?
};
new_vertex.insert(edge.start_vertex_id, seam_point);
new_vertex.insert(edge.end_vertex_id, seam_point);
new_curve.insert(edge.id, rim);
} else {
let previous =
&loop_record.coedges[(coedge_index + coedge_count - 1) % coedge_count];
let next = &loop_record.coedges[(coedge_index + 1) % coedge_count];
let (at_start, at_end) = if coedge.forward {
(previous, next)
} else {
(next, previous)
};
let mut resolve = |adjacent_coedge: &CoedgeRecord,
vertex_id: u64|
-> Result<RimEnd, String> {
let adjacent = *edge_by_id.get(&adjacent_coedge.edge_id).ok_or_else(|| {
format!(
"offset_ruled_face: missing edge {}",
adjacent_coedge.edge_id
)
})?;
let old_point = vertex_pos
.get(&vertex_id)
.copied()
.ok_or_else(|| format!("offset_ruled_face: missing vertex {vertex_id}"))?;
let (end, point) = resolve_open_rim_end(
solid,
face_id,
&faces_of_edge,
&rim,
adjacent,
old_point,
plane_tolerance,
)?;
match new_vertex.get(&vertex_id).copied() {
Some(existing) if existing.sub(point).length() > plane_tolerance => {
return Err(
"offset_ruled_face: the two rims meeting at a multi-loop corner \
disagree on its new position — refusing"
.into(),
)
}
Some(_) => {}
None => {
new_vertex.insert(vertex_id, point);
}
}
Ok(end)
};
let start = resolve(at_start, edge.start_vertex_id)?;
let end = resolve(at_end, edge.end_vertex_id)?;
open_rims.push(OpenRim {
edge_id: edge.id,
conic: rim,
start,
end,
old_mid,
start_vertex_id: edge.start_vertex_id,
end_vertex_id: edge.end_vertex_id,
});
}
if is_plane {
cap_faces.insert(neighbour);
} else if is_coaxial_ruled {
ruled_faces.insert(neighbour);
} else {
curved_faces.insert(neighbour);
}
}
}
for rim in &open_rims {
let [d0, d1] = rim.conic.domain()?;
let middle = project_point_to_curve(&rim.conic, rim.old_mid)?.u;
let (from, to) = match (rim.start, rim.end) {
(RimEnd::Corner(a), RimEnd::Corner(b)) => {
let (low, high) = if a <= b { (a, b) } else { (b, a) };
if middle < low || middle > high {
return Err(
"offset_ruled_face: a multi-loop rim arc wraps the pushed carrier's \
periodic seam — deferred (refusing)"
.into(),
);
}
(low, high)
}
(RimEnd::Seam, RimEnd::Corner(corner)) | (RimEnd::Corner(corner), RimEnd::Seam) => {
if middle < corner {
(d0, corner)
} else {
(corner, d1)
}
}
(RimEnd::Seam, RimEnd::Seam) => {
return Err(
"offset_ruled_face: a multi-loop rim arc ends on the seam at BOTH ends — \
refusing"
.into(),
)
}
};
let mut trimmed = subcurve(&rim.conic, from, to)?;
let start_point = *new_vertex.get(&rim.start_vertex_id).ok_or_else(|| {
"offset_ruled_face: a multi-loop rim corner was not relocated — refusing".to_string()
})?;
let end_point = *new_vertex.get(&rim.end_vertex_id).ok_or_else(|| {
"offset_ruled_face: a multi-loop rim corner was not relocated — refusing".to_string()
})?;
let [t0, _] = trimmed.domain()?;
let head = trimmed.evaluate(t0)?;
if head.sub(start_point).length() > head.sub(end_point).length() {
trimmed = trimmed.reversed()?;
}
new_curve.insert(rim.edge_id, trimmed);
}
let pos = |vid: u64| -> Vec3 {
new_vertex
.get(&vid)
.copied()
.unwrap_or_else(|| vertex_pos[&vid])
};
for seam_id in &seam_edges {
let seam = *edge_by_id
.get(seam_id)
.ok_or_else(|| format!("offset_ruled_face: missing seam edge {seam_id}"))?;
let start = *new_vertex.get(&seam.start_vertex_id).ok_or_else(|| {
"offset_ruled_face: seam endpoint was not relocated by a rim — refusing".to_string()
})?;
let end = *new_vertex.get(&seam.end_vertex_id).ok_or_else(|| {
"offset_ruled_face: seam endpoint was not relocated by a rim — refusing".to_string()
})?;
new_curve.insert(*seam_id, make_line(start, end)?);
}
for ruled in &ruled_faces {
let (nshell, nface) = find_face(solid, *ruled)
.ok_or_else(|| format!("offset_ruled_face: missing ruled neighbour {ruled}"))?;
for loop_record in &solid.shells[nshell].faces[nface].loops {
for coedge in &loop_record.coedges {
let edge = *edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
format!("offset_ruled_face: missing edge {}", coedge.edge_id)
})?;
let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
let is_seam = incident.iter().all(|f| *f == *ruled);
let touches_moved = new_vertex.contains_key(&edge.start_vertex_id)
|| new_vertex.contains_key(&edge.end_vertex_id);
if is_seam && touches_moved && !new_curve.contains_key(&edge.id) {
new_curve.insert(
edge.id,
make_line(pos(edge.start_vertex_id), pos(edge.end_vertex_id))?,
);
}
}
}
}
let mut new_range: HashMap<u64, (f64, f64)> = HashMap::default();
for curved in &curved_faces {
let (nshell, nface) = find_face(solid, *curved)
.ok_or_else(|| format!("offset_ruled_face: missing curved neighbour {curved}"))?;
for loop_record in &solid.shells[nshell].faces[nface].loops {
for coedge in &loop_record.coedges {
let edge = *edge_by_id.get(&coedge.edge_id).ok_or_else(|| {
format!("offset_ruled_face: missing edge {}", coedge.edge_id)
})?;
if new_curve.contains_key(&edge.id) || new_range.contains_key(&edge.id) {
continue;
}
let start_moved = new_vertex.get(&edge.start_vertex_id).copied();
let end_moved = new_vertex.get(&edge.end_vertex_id).copied();
if start_moved.is_none() && end_moved.is_none() {
continue;
}
if edge.start_vertex_id == edge.end_vertex_id {
return Err(format!(
"offset_ruled_face: the push moved the vertex of curved neighbour \
{curved}'s CLOSED edge {} — deferred (refusing)",
edge.id
));
}
let mut range = (edge.t0, edge.t1);
for (moved, slot) in [(start_moved, 0usize), (end_moved, 1usize)] {
let Some(point) = moved else { continue };
let projection = project_point_to_curve(&edge.curve, point)?;
if projection.distance > plane_tolerance {
return Err(format!(
"offset_ruled_face: a relocated rim vertex left curved neighbour \
{curved}'s edge {} (off by {:.3e}) — refusing",
edge.id, projection.distance
));
}
if slot == 0 {
range.0 = projection.u;
} else {
range.1 = projection.u;
}
}
new_range.insert(edge.id, range);
}
}
}
if !curved_faces.is_empty() {
for edge in &solid.edges {
if new_curve.contains_key(&edge.id) || new_range.contains_key(&edge.id) {
continue;
}
if new_vertex.contains_key(&edge.start_vertex_id)
|| new_vertex.contains_key(&edge.end_vertex_id)
{
return Err(format!(
"offset_ruled_face: relocating a curved neighbour's rim moved the end of \
edge {}, which this push does not rebuild — refusing",
edge.id
));
}
}
}
if !open_rims.is_empty() {
let mut corner_edges: Vec<(u64, NurbsCurve)> = Vec::new();
for edge in &solid.edges {
if new_curve.contains_key(&edge.id) {
continue;
}
if !new_vertex.contains_key(&edge.start_vertex_id)
&& !new_vertex.contains_key(&edge.end_vertex_id)
{
continue;
}
if edge.curve.degree != 1 || edge.curve.control_points.len() != 2 {
return Err(
"offset_ruled_face: the push moved the end of a CURVED edge that is not a \
rebuilt rim — refusing"
.into(),
);
}
let start = pos(edge.start_vertex_id);
let end = pos(edge.end_vertex_id);
for neighbour in faces_of_edge.get(&edge.id).cloned().unwrap_or_default() {
if !cap_faces.contains(&neighbour) {
return Err(
"offset_ruled_face: a relocated corner borders a face this push does not \
re-trim — refusing"
.into(),
);
}
let (nshell, nface) = find_face(solid, neighbour).ok_or_else(|| {
format!("offset_ruled_face: missing neighbour {neighbour}")
})?;
let plane = plane_of_surface(
&solid.shells[nshell].faces[nface].surface,
plane_tolerance,
"offset_ruled_face",
)?;
for point in [start, end] {
if point.sub(plane.origin).dot(plane.normal).abs() > plane_tolerance {
return Err(
"offset_ruled_face: a relocated corner left one of its fixed \
neighbour planes — refusing"
.into(),
);
}
}
}
corner_edges.push((edge.id, make_line(start, end)?));
}
for (edge_id, curve) in corner_edges {
new_curve.insert(edge_id, curve);
}
}
let changed_edges: HashSet<u64> = new_curve
.keys()
.chain(new_range.keys())
.copied()
.collect();
let mut result = solid.clone();
for edge in &mut result.edges {
if let Some(curve) = new_curve.get(&edge.id) {
let [d0, d1] = curve.domain()?;
edge.curve = curve.clone();
edge.t0 = d0;
edge.t1 = d1;
} else if let Some((t0, t1)) = new_range.get(&edge.id) {
edge.t0 = *t0;
edge.t1 = *t1;
}
}
for vertex in &mut result.vertices {
if let Some(point) = new_vertex.get(&vertex.id) {
vertex.point = *point;
}
}
result.shells[pshell].faces[pface].surface = s_prime;
let final_edges: HashMap<u64, EdgeRecord> =
result.edges.iter().map(|e| (e.id, e.clone())).collect();
if !ruled_faces.is_empty() {
retrim_offset_ruled_face(&mut result, face_id, &final_edges, tolerance)?;
for ruled in &ruled_faces {
retrim_offset_ruled_face(&mut result, *ruled, &final_edges, tolerance)?;
}
for curved in &curved_faces {
refit_changed_pcurves(&mut result, *curved, &final_edges, &changed_edges, tolerance)?;
}
} else if !curved_faces.is_empty() {
let (gshell, gface) = find_face(&result, face_id)
.ok_or_else(|| format!("offset_ruled_face: missing face {face_id}"))?;
let samples = boundary_samples(
&result.shells[gshell].faces[gface],
&final_edges,
"offset_ruled_face",
)?;
extend_ruled_neighbour_over(&mut result, face_id, &samples, tolerance)?;
refit_changed_pcurves(&mut result, face_id, &final_edges, &changed_edges, tolerance)?;
for curved in &curved_faces {
refit_changed_pcurves(&mut result, *curved, &final_edges, &changed_edges, tolerance)?;
}
} else if !rim_edges.is_empty() {
let rebuilt: HashSet<u64> = rim_edges.iter().copied().collect();
let surface = result.shells[pshell].faces[pface].surface.clone();
let [u_start, u_end] = surface.domain_u()?;
let u_period = u_end - u_start;
for loop_record in &mut result.shells[pshell].faces[pface].loops {
for coedge in &mut loop_record.coedges {
if !rebuilt.contains(&coedge.edge_id) {
continue;
}
let edge = final_edges
.get(&coedge.edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {}", coedge.edge_id))?;
let mut pcurve = build_pcurve_on_surface(&surface, &edge.curve)?;
if !coedge.forward {
pcurve = pcurve.reversed()?;
}
coedge.pcurve = reanchor_pcurve_u(&pcurve, &coedge.pcurve, u_period)?;
}
}
}
for cap in &cap_faces {
let (cshell, cface) = find_face(&result, *cap)
.ok_or_else(|| format!("offset_ruled_face: missing cap {cap}"))?;
let plane = plane_of_surface(
&result.shells[cshell].faces[cface].surface,
plane_tolerance,
"offset_ruled_face",
)?;
retrim_planar_face(
&mut result.shells[cshell].faces[cface],
&plane,
&final_edges,
scale,
"offset_ruled_face",
)?;
}
let issues = result.validate();
if !issues.is_empty() {
if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
let (pshell, pface) = find_face(&result, face_id).expect("pushed face survives");
let face = &result.shells[pshell].faces[pface];
for (li, loop_record) in face.loops.iter().enumerate() {
for coedge in &loop_record.coedges {
let Some(edge) = result.edges.iter().find(|e| e.id == coedge.edge_id) else {
continue;
};
let (Ok([p0, p1]), Ok(c0)) = (coedge.pcurve.domain(), edge.curve.evaluate(edge.t0))
else {
continue;
};
let mut worst = 0.0f64;
for k in 0..=32 {
let f = k as f64 / 32.0;
let t = if coedge.forward {
edge.t0 + (edge.t1 - edge.t0) * f
} else {
edge.t1 - (edge.t1 - edge.t0) * f
};
if let (Ok(q), Ok(target)) =
(coedge.pcurve.evaluate(p0 + (p1 - p0) * f), edge.curve.evaluate(t))
{
if let Ok(on_s) = face.surface.evaluate(q.x, q.y) {
worst = worst.max(on_s.sub(target).length());
}
}
}
let (Ok(a), Ok(b)) = (coedge.pcurve.evaluate(p0), coedge.pcurve.evaluate(p1)) else {
continue;
};
eprintln!(
"PUSH-DEBUG face {} loop {li} edge {} fwd={} rebuilt={} t=[{:.4},{:.4}] dom={:?} \
curve(t0)=({:.4},{:.4},{:.4}) pcurve ({:.4},{:.4})->({:.4},{:.4}) dev={worst:.4}",
face.id,
edge.id,
coedge.forward,
rim_edges.contains(&edge.id),
edge.t0,
edge.t1,
edge.curve.domain().ok(),
c0.x,
c0.y,
c0.z,
a.x,
a.y,
b.x,
b.y
);
}
}
}
return Err(format!(
"offset_ruled_face: pushed solid failed validation: {issues:?}"
));
}
if let (Ok(before), Ok(after)) =
(solid_signed_volume(solid), solid_signed_volume(&result))
{
if before * after <= 0.0 {
return Err("offset_ruled_face: the push inverts the solid — refusing".into());
}
}
Ok(result)
}
#[derive(Clone, Copy)]
enum RimEnd {
Corner(f64),
Seam,
}
struct OpenRim {
edge_id: u64,
conic: NurbsCurve,
start: RimEnd,
end: RimEnd,
old_mid: Vec3,
start_vertex_id: u64,
end_vertex_id: u64,
}
fn resolve_open_rim_end(
solid: &BrepSolid,
face_id: u64,
faces_of_edge: &HashMap<u64, Vec<u64>>,
conic: &NurbsCurve,
adjacent: &EdgeRecord,
old_point: Vec3,
plane_tolerance: f64,
) -> Result<(RimEnd, Vec3), String> {
let incident = faces_of_edge.get(&adjacent.id).cloned().unwrap_or_default();
if incident.iter().all(|f| *f == face_id) {
let [d0, _] = conic.domain()?;
return Ok((RimEnd::Seam, conic.evaluate(d0)?));
}
let other = *incident
.iter()
.find(|f| **f != face_id)
.ok_or_else(|| format!("offset_ruled_face: edge {} has no neighbour", adjacent.id))?;
let (nshell, nface) = find_face(solid, other)
.ok_or_else(|| format!("offset_ruled_face: missing neighbour {other}"))?;
let plane = plane_of_surface(
&solid.shells[nshell].faces[nface].surface,
plane_tolerance,
"offset_ruled_face",
)?;
if curve_lies_in_plane(conic, &plane, plane_tolerance)? {
let projection = project_point_to_curve(conic, old_point)?;
return Ok((RimEnd::Corner(projection.u), conic.evaluate(projection.u)?));
}
let mut best: Option<(f64, f64)> = None;
for parameter in plane_crossing_params(conic, &plane)? {
let distance = conic.evaluate(parameter)?.sub(old_point).length();
if best.map(|(best, _)| distance < best).unwrap_or(true) {
best = Some((distance, parameter));
}
}
let (_, parameter) = best.ok_or_else(|| {
"offset_ruled_face: a multi-loop rim corner no longer meets its adjacent neighbour \
(the push pulled the window off it) — refusing"
.to_string()
})?;
Ok((RimEnd::Corner(parameter), conic.evaluate(parameter)?))
}
fn curve_lies_in_plane(
curve: &NurbsCurve,
plane: &Plane,
plane_tolerance: f64,
) -> Result<bool, String> {
let [d0, d1] = curve.domain()?;
for step in 0..=16 {
let t = d0 + (d1 - d0) * step as f64 / 16.0;
if curve
.evaluate(t)?
.sub(plane.origin)
.dot(plane.normal)
.abs()
> plane_tolerance
{
return Ok(false);
}
}
Ok(true)
}
fn plane_crossing_params(curve: &NurbsCurve, plane: &Plane) -> Result<Vec<f64>, String> {
let [d0, d1] = curve.domain()?;
let signed = |t: f64| -> Result<f64, String> {
Ok(curve.evaluate(t)?.sub(plane.origin).dot(plane.normal))
};
const SAMPLES: usize = 512;
let mut roots = Vec::new();
let mut previous = (d0, signed(d0)?);
for index in 1..=SAMPLES {
let t = d0 + (d1 - d0) * index as f64 / SAMPLES as f64;
let value = signed(t)?;
if previous.1 == 0.0 {
roots.push(previous.0);
} else if (previous.1 < 0.0) != (value < 0.0) {
let (mut low, mut high) = (previous.0, t);
let mut low_value = previous.1;
for _ in 0..100 {
let middle = 0.5 * (low + high);
if middle <= low || middle >= high {
break;
}
let middle_value = signed(middle)?;
if (low_value < 0.0) != (middle_value < 0.0) {
high = middle;
} else {
low = middle;
low_value = middle_value;
}
}
roots.push(0.5 * (low + high));
}
previous = (t, value);
}
if previous.1 == 0.0 {
roots.push(previous.0);
}
Ok(roots)
}
fn subcurve(curve: &NurbsCurve, from: f64, to: f64) -> Result<NurbsCurve, String> {
let [d0, d1] = curve.domain()?;
let span = (d1 - d0).max(1e-12);
if to - from <= 1e-9 * span {
return Err("offset_ruled_face: a rebuilt rim arc collapsed to a point — refusing".into());
}
let mut trimmed = curve.clone();
if to < d1 - 1e-9 * span {
trimmed = trimmed.split(to)?.0;
}
if from > d0 + 1e-9 * span {
trimmed = trimmed.split(from)?.1;
}
Ok(trimmed)
}
fn match_closed_rim_direction(
rim: NurbsCurve,
previous: &EdgeRecord,
) -> Result<NurbsCurve, String> {
let [d0, _] = rim.domain()?;
let incoming = previous.curve.derivatives(previous.t0, 1)?;
let rebuilt = rim.derivatives(d0, 1)?;
if incoming[1].dot(rebuilt[1]) < 0.0 {
return rim.reversed();
}
Ok(rim)
}
fn reanchor_pcurve_u(
pcurve: &NurbsCurve,
previous: &NurbsCurve,
u_period: f64,
) -> Result<NurbsCurve, String> {
if !(u_period.is_finite() && u_period > 0.0) {
return Ok(pcurve.clone());
}
let [a0, a1] = pcurve.domain()?;
let [b0, b1] = previous.domain()?;
let shift = ((previous.evaluate(b0)?.x - pcurve.evaluate(a0)?.x) / u_period).round() * u_period;
let shifted = if shift == 0.0 {
pcurve.clone()
} else {
let controls = pcurve
.control_points
.iter()
.map(|control| {
let mut point = control.point()?;
point.x += shift;
Ok(crate::Vec4::from_point(point, control.w))
})
.collect::<Result<Vec<_>, String>>()?;
NurbsCurve::new(pcurve.degree, pcurve.knots.clone(), controls)?
};
let end_drift = (shifted.evaluate(a1)?.x - previous.evaluate(b1)?.x).abs();
if end_drift > 0.25 * u_period {
return Err(format!(
"offset_ruled_face: a rebuilt rim traverses the carrier's periodic parameter \
differently from the edge it replaces (end drift {end_drift}) — refusing"
));
}
Ok(shifted)
}
pub(super) fn ruled_neighbour_is_coaxial(
neighbour: &NurbsSurface,
axis_origin: Vec3,
axis_dir: Vec3,
scale: f64,
) -> bool {
let Some(AnalyticSurface::RuledRevolution { frame, .. }) = neighbour.analytic() else {
return false;
};
if frame.axis.dot(axis_dir).abs() < 1.0 - 1e-9 {
return false;
}
let offset = frame.origin.sub(axis_origin);
let perpendicular = offset.sub(axis_dir.scale(offset.dot(axis_dir)));
perpendicular.length() <= 1e-9 * scale.max(1.0)
}
pub(super) fn retrim_offset_ruled_face(
result: &mut BrepSolid,
face_id: u64,
final_edges: &HashMap<u64, EdgeRecord>,
tolerance: f64,
) -> Result<(), String> {
let (shell, face_pos) = find_face(result, face_id)
.ok_or_else(|| format!("offset_ruled_face: missing ruled face {face_id}"))?;
retrim_face_in_solid(
result,
shell,
face_pos,
final_edges,
|solid, points| extend_ruled_neighbour_over(solid, face_id, points, tolerance),
PcurveFit::SubrangeAware { tolerance },
"offset_ruled_face",
)
}
struct SingleNeighbourHole {
neighbour: u64,
edge_ids: Vec<u64>,
pinned: Vec<(u64, u64)>,
}
fn seam_axial_plane(surface: &NurbsSurface, seam: &EdgeRecord) -> Option<Plane> {
let structure = crate::revolution_structure(surface)?;
let midpoint = seam
.curve
.evaluate(0.5 * (seam.t0 + seam.t1))
.ok()?;
let offset = midpoint.sub(structure.frame.origin);
let radial = offset
.sub(structure.frame.axis.scale(offset.dot(structure.frame.axis)))
.normalized()
.ok()?;
let normal = structure.frame.axis.cross(radial).normalized().ok()?;
Some(Plane {
origin: structure.frame.origin,
u_dir: structure.frame.axis,
v_dir: radial,
normal,
})
}
#[allow(clippy::too_many_arguments)]
fn single_neighbour_hole(
loop_record: &crate::topology::LoopRecord,
face_id: u64,
solid: &BrepSolid,
faces_of_edge: &HashMap<u64, Vec<u64>>,
edge_by_id: &HashMap<u64, &EdgeRecord>,
frame_origin: Vec3,
frame_axis: Vec3,
scale: f64,
) -> Result<Option<SingleNeighbourHole>, String> {
let debug = std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok();
let mut neighbour: Option<u64> = None;
let mut edge_ids: Vec<u64> = Vec::new();
for coedge in &loop_record.coedges {
let edge = *edge_by_id
.get(&coedge.edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {}", coedge.edge_id))?;
if edge.start_vertex_id == edge.end_vertex_id {
if debug { eprintln!("HOLE reject: edge {} is closed", edge.id); }
return Ok(None); }
let incident = faces_of_edge.get(&edge.id).cloned().unwrap_or_default();
let others: Vec<u64> = incident.into_iter().filter(|f| *f != face_id).collect();
if others.len() != 1 {
if debug { eprintln!("HOLE reject: edge {} has {} others", edge.id, others.len()); }
return Ok(None); }
match neighbour {
Some(known) if known != others[0] => {
if debug { eprintln!("HOLE reject: mixed neighbours {known} / {}", others[0]); }
return Ok(None);
}
Some(_) => {}
None => neighbour = Some(others[0]),
}
if !edge_ids.contains(&edge.id) {
edge_ids.push(edge.id);
}
}
let Some(neighbour) = neighbour else {
return Ok(None);
};
if edge_ids.len() < 2 {
if debug { eprintln!("HOLE reject: only {} edges", edge_ids.len()); }
return Ok(None);
}
let (nshell, nface) = find_face(solid, neighbour)
.ok_or_else(|| format!("offset_ruled_face: missing neighbour {neighbour}"))?;
let surface = &solid.shells[nshell].faces[nface].surface;
if matches!(surface.analytic(), Some(AnalyticSurface::Plane { .. }))
|| ruled_neighbour_is_coaxial(surface, frame_origin, frame_axis, scale)
{
if debug { eprintln!("HOLE reject: neighbour {neighbour} is planar/coaxial"); }
return Ok(None); }
let neighbour_edges: HashSet<u64> = solid.shells[nshell].faces[nface]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
.collect();
let mut pinned: Vec<(u64, u64)> = Vec::new();
for edge_id in &edge_ids {
let edge = *edge_by_id
.get(edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {edge_id}"))?;
for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
for other in &solid.edges {
if other.start_vertex_id != vertex_id && other.end_vertex_id != vertex_id {
continue;
}
if edge_ids.contains(&other.id) {
continue;
}
if !neighbour_edges.contains(&other.id) {
if debug {
eprintln!("HOLE reject: vertex {vertex_id} also on foreign edge {}", other.id);
}
return Ok(None);
}
if pinned
.iter()
.any(|(known, pin)| *known == vertex_id && *pin != other.id)
{
if debug {
eprintln!("HOLE reject: vertex {vertex_id} pinned by two neighbour edges");
}
return Ok(None);
}
if !pinned.iter().any(|(known, _)| *known == vertex_id) {
pinned.push((vertex_id, other.id));
}
}
}
}
if debug {
eprintln!("HOLE accept: neighbour {neighbour} edges {edge_ids:?} pinned {pinned:?}");
}
Ok(Some(SingleNeighbourHole {
neighbour,
edge_ids,
pinned,
}))
}
#[allow(clippy::too_many_arguments)]
fn rebuild_single_neighbour_hole(
solid: &BrepSolid,
s_prime: &NurbsSurface,
hole: &SingleNeighbourHole,
edge_by_id: &HashMap<u64, &EdgeRecord>,
vertex_pos: &HashMap<u64, Vec3>,
tolerance: f64,
scale: f64,
new_curve: &mut HashMap<u64, NurbsCurve>,
new_vertex: &mut HashMap<u64, Vec3>,
) -> Result<(), String> {
let (nshell, nface) = find_face(solid, hole.neighbour)
.ok_or_else(|| format!("offset_ruled_face: missing neighbour {}", hole.neighbour))?;
let neighbour_surface = &solid.shells[nshell].faces[nface].surface;
let mut seeds: Vec<Vec3> = Vec::new();
let mut reference: Vec<Vec3> = Vec::new();
for edge_id in &hole.edge_ids {
let edge = *edge_by_id
.get(edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {edge_id}"))?;
let samples = edge_seeds(edge, 9)?;
reference.extend(samples.iter().copied());
seeds.extend(samples);
}
let policy = MarchPolicy {
tolerance,
residual_tolerance: (scale * 5e-4).max(5e-6),
seeds,
};
let found = match reintersect_carriers(s_prime, neighbour_surface, &policy) {
Ok(found) => found,
Err(ReintersectRefusal::Separated) => {
return Err(
"offset_ruled_face: the pushed carrier no longer meets a neighbour \
(the push separated them, or the rim left the neighbour's domain) — refusing"
.into(),
)
}
Err(other) => return Err(format!("offset_ruled_face: {}", other.describe())),
};
if found.lane != RimLane::Marched {
return Err(format!(
"offset_ruled_face: the window bounded by face {} re-intersects in CLOSED FORM, \
whose arc rebuild is the analytic lane's — deferred (refusing)",
hole.neighbour
));
}
if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
eprintln!(
"HOLE rebuild neighbour {}: lane {:?}, {} branch(es), residual {:.3e} \
(gate {:.3e})",
hole.neighbour,
found.lane,
found.sections.len(),
found.residual,
policy.residual_tolerance
);
}
let section = found
.nearest_section(&reference)
.map_err(|error| format!("offset_ruled_face: {error}"))?;
for edge_id in &hole.edge_ids {
let edge = *edge_by_id
.get(edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {edge_id}"))?;
for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
if new_vertex.contains_key(&vertex_id) {
continue;
}
let old = *vertex_pos
.get(&vertex_id)
.ok_or_else(|| format!("offset_ruled_face: missing vertex {vertex_id}"))?;
let point = match hole
.pinned
.iter()
.find(|(known, _)| *known == vertex_id)
.map(|(_, pin)| *pin)
{
Some(pin) => {
let seam = *edge_by_id
.get(&pin)
.ok_or_else(|| format!("offset_ruled_face: missing edge {pin}"))?;
let plane = seam_axial_plane(neighbour_surface, seam).ok_or_else(|| {
format!(
"offset_ruled_face: the window's corner is pinned to edge {pin} of a \
neighbour that is not a surface of revolution — refusing"
)
})?;
let reach = reference
.iter()
.map(|point| point.sub(old).length())
.fold(0.0f64, f64::max)
.max(tolerance * 100.0);
curve_plane_crossing_near(§ion.curve, &plane, old, reach)
.map(|(_, point)| point)
.ok_or_else(|| {
format!(
"offset_ruled_face: the rebuilt window never crosses the seam \
(edge {pin}) its corner is pinned to — refusing"
)
})?
}
None => section_corner(section, old)
.map_err(|error| format!("offset_ruled_face: {error}"))?,
};
new_vertex.insert(vertex_id, point);
}
}
for edge_id in &hole.edge_ids {
let edge = *edge_by_id
.get(edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {edge_id}"))?;
let from = new_vertex[&edge.start_vertex_id];
let to = new_vertex[&edge.end_vertex_id];
let through = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1))?;
let arc = arc_of_section(section, from, to, through, tolerance)
.map_err(|error| format!("offset_ruled_face: {error}"))?;
new_curve.insert(edge.id, arc);
}
Ok(())
}
fn refit_changed_pcurves(
result: &mut BrepSolid,
face_id: u64,
final_edges: &HashMap<u64, EdgeRecord>,
changed: &HashSet<u64>,
tolerance: f64,
) -> Result<(), String> {
let (shell, face_pos) = find_face(result, face_id)
.ok_or_else(|| format!("offset_ruled_face: missing face {face_id}"))?;
let surface = result.shells[shell].faces[face_pos].surface.clone();
let [u_start, u_end] = surface.domain_u()?;
let u_period = u_end - u_start;
for loop_record in &mut result.shells[shell].faces[face_pos].loops {
for coedge in &mut loop_record.coedges {
if !changed.contains(&coedge.edge_id) {
continue;
}
let edge = final_edges
.get(&coedge.edge_id)
.ok_or_else(|| format!("offset_ruled_face: missing edge {}", coedge.edge_id))?;
let [d0, d1] = edge.curve.domain()?;
let span = (d1 - d0).max(1e-12);
let is_subrange =
(edge.t0 - d0).abs() > 1e-9 * span || (edge.t1 - d1).abs() > 1e-9 * span;
let pcurve = if is_subrange {
build_pcurve_on_surface_range(
&surface,
&edge.curve,
edge.t0,
edge.t1,
coedge.forward,
tolerance,
)?
} else {
let mut pcurve = build_pcurve_on_surface(&surface, &edge.curve)?;
if !coedge.forward {
pcurve = pcurve.reversed()?;
}
pcurve
};
coedge.pcurve = reanchor_pcurve_u(&pcurve, &coedge.pcurve, u_period)?;
}
}
Ok(())
}
fn nearest_curve(curves: &[NurbsCurve], reference: Vec3) -> Result<NurbsCurve, String> {
let mut best: Option<(f64, &NurbsCurve)> = None;
for curve in curves {
let mid = curve.evaluate(0.5)?;
let d = mid.sub(reference).length();
if best.map(|(best_d, _)| d < best_d).unwrap_or(true) {
best = Some((d, curve));
}
}
best.map(|(_, curve)| curve.clone())
.ok_or_else(|| "offset_ruled_face: empty intersection".into())
}