use super::*;
#[path = "face_offset_plan.rs"]
mod plan;
use plan::RebuildPlan;
#[path = "face_offset_tangent.rs"]
mod tangent;
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());
}
if let Some(directory) = census_directory() {
record_push_census(&directory, solid, face_id, distance);
}
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 tangents =
tangent::tangent_vertices(solid, face_id, &faces_of_edge, &edge_by_id, plane_tolerance)?;
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 (a, b) = (rim.start.parameter(), rim.end.parameter());
let (low, high) = if a <= b { (a, b) } else { (b, a) };
if std::env::var("BREP_PUSH_HOLE_DEBUG").is_ok() {
let describe = |end: RimEnd| match end {
RimEnd::Corner(u) => format!("Corner({u:.9})"),
RimEnd::Seam(u) => format!("Seam({u:.9})"),
};
let at = |vid: u64| {
new_vertex
.get(&vid)
.map(|p: &Vec3| format!("({:.5},{:.5},{:.5})", p.x, p.y, p.z))
.unwrap_or_else(|| "-".to_string())
};
eprintln!(
"OPENRIM edge {} domain=[{d0},{d1}] start={} end={} middle={middle:.9} \
v{}{} -> v{}{}",
rim.edge_id,
describe(rim.start),
describe(rim.end),
rim.start_vertex_id,
at(rim.start_vertex_id),
rim.end_vertex_id,
at(rim.end_vertex_id),
);
}
if let (RimEnd::Seam(_), RimEnd::Seam(_)) = (rim.start, rim.end) {
return Err(
"offset_ruled_face: a multi-loop rim arc ends on the seam at BOTH ends — \
refusing"
.into(),
);
}
let mut trimmed = rim_arc_between(&rim.conic, low, high, middle, tolerance)?;
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 mut plan = RebuildPlan::default();
tangent::plan_tangent_vertices(
solid,
face_id,
&tangents,
&faces_of_edge,
&edge_by_id,
&vertex_pos,
&s_prime,
plane_tolerance,
&mut new_vertex,
&mut cap_faces,
&mut plan,
)?;
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.straight_segment(tolerance).is_none() {
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) {
let stuck = [edge.start_vertex_id, edge.end_vertex_id]
.into_iter()
.find(|vertex| new_vertex.contains_key(vertex))
.unwrap_or(edge.start_vertex_id);
return Err(format!(
"offset_ruled_face: the push relocates rim corner vertex {stuck}, which \
also ends edge {} of face {neighbour} — a face this push does not \
re-trim, so the rim would tear away from it (refusing)",
edge.id
));
}
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);
}
}
plan.curves = new_curve;
plan.ranges = new_range;
plan.moved = new_vertex;
let (mut result, applied) = plan.apply(solid)?;
let changed_edges = plan.changed_edges(&applied);
let (pshell, pface) = find_face(&result, face_id)
.ok_or_else(|| format!("offset_ruled_face: missing face {face_id}"))?;
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 seams: HashSet<u64> = seam_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 loop_record in &mut result.shells[pshell].faces[pface].loops {
for coedge in &mut loop_record.coedges {
if !seams.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))?;
coedge.pcurve = reseat_seam_pcurve(&surface, &coedge.pcurve, edge, coedge.forward)?;
}
}
}
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)
}
fn reseat_seam_pcurve(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Result<NurbsCurve, String> {
let [q0, q1] = pcurve.domain()?;
let (head, tail) = (pcurve.evaluate(q0)?, pcurve.evaluate(q1)?);
let [v0, v1] = surface.domain_v()?;
let station = |u: f64, target: Vec3| -> Result<f64, String> {
let base = surface.evaluate(u, v0)?;
let top = surface.evaluate(u, v1)?;
let direction = top.sub(base);
let length_squared = direction.length_squared();
if length_squared <= 0.0 {
return Err(
"offset_ruled_face: the pushed carrier's generatrix has zero length — refusing"
.into(),
);
}
Ok(v0 + (v1 - v0) * (target.sub(base).dot(direction) / length_squared))
};
let (at_head, at_tail) = if forward {
(edge.curve.evaluate(edge.t0)?, edge.curve.evaluate(edge.t1)?)
} else {
(edge.curve.evaluate(edge.t1)?, edge.curve.evaluate(edge.t0)?)
};
let reseated = crate::make_line(
Vec3::new(head.x, station(head.x, at_head)?, 0.0),
Vec3::new(tail.x, station(tail.x, at_tail)?, 0.0),
)?;
let [r0, r1] = reseated.domain()?;
for (parameter, target) in [(r0, at_head), (r1, at_tail)] {
let uv = reseated.evaluate(parameter)?;
let drift = surface.evaluate(uv.x, uv.y)?.sub(target).length();
if drift > 1e-6 * (1.0 + target.length()) {
return Err(format!(
"offset_ruled_face: the reseated seam pcurve misses its own chord by \
{drift:.3e} — refusing"
));
}
}
Ok(reseated)
}
#[derive(Clone, Copy)]
enum RimEnd {
Corner(f64),
Seam(f64),
}
impl RimEnd {
fn parameter(self) -> f64 {
match self {
RimEnd::Corner(parameter) | RimEnd::Seam(parameter) => parameter,
}
}
}
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 (pshell, pface) = find_face(solid, face_id)
.ok_or_else(|| format!("offset_ruled_face: missing pushed face {face_id}"))?;
let meridian = seam_axial_plane(&solid.shells[pshell].faces[pface].surface, adjacent)
.ok_or_else(|| {
format!(
"offset_ruled_face: the pushed face's seam (edge {}) is not a meridian of \
its carrier — refusing",
adjacent.id
)
})?;
let parameter = conic_crossing_nearest(conic, &meridian, old_point, plane_tolerance)?
.ok_or_else(|| {
format!(
"offset_ruled_face: the rebuilt rim never crosses the pushed face's own seam \
meridian (edge {}) — refusing",
adjacent.id
)
})?;
return Ok((RimEnd::Seam(parameter), conic.evaluate(parameter)?));
}
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)?));
}
census_push("corner_crossings", || {
let [d0, d1] = conic.domain().unwrap_or([f64::NAN, f64::NAN]);
let height = |t: f64| conic.evaluate(t).map(|p| p.sub(plane.origin).dot(plane.normal)).unwrap_or(f64::NAN);
serde_json::json!({
"h_start": height(d0),
"h_end": height(d1),
"roots": plane_crossing_params(conic, &plane).map(|roots| roots.iter().map(|t| (t - d0) / (d1 - d0)).collect::<Vec<_>>()).unwrap_or_default(),
"closed": conic.evaluate(d0).and_then(|a| conic.evaluate(d1).map(|b| a.sub(b).length())).unwrap_or(f64::NAN) <= plane_tolerance,
})
});
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 conic_crossing_nearest(
curve: &NurbsCurve,
plane: &Plane,
reference: Vec3,
plane_tolerance: f64,
) -> Result<Option<f64>, String> {
let [d0, d1] = curve.domain()?;
let span = (d1 - d0).max(1e-12);
let mut candidates: Vec<f64> = Vec::new();
let mut ends = [false, false];
for (slot, end) in [d0, d1].into_iter().enumerate() {
if curve.evaluate(end)?.sub(plane.origin).dot(plane.normal).abs() <= plane_tolerance {
ends[slot] = true;
candidates.push(end);
}
}
for root in plane_crossing_params(curve, plane)? {
if (ends[0] && (root - d0).abs() <= 1e-6 * span)
|| (ends[1] && (root - d1).abs() <= 1e-6 * span)
{
continue;
}
candidates.push(root);
}
let mut best: Option<(f64, f64)> = None;
for parameter in candidates {
let distance = curve.evaluate(parameter)?.sub(reference).length();
if best.map(|(known, _)| distance < known).unwrap_or(true) {
best = Some((distance, parameter));
}
}
Ok(best.map(|(_, parameter)| parameter))
}
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 rim_arc_between(
conic: &NurbsCurve,
low: f64,
high: f64,
middle: f64,
tolerance: f64,
) -> Result<NurbsCurve, String> {
let [d0, d1] = conic.domain()?;
if middle >= low && middle <= high {
return subcurve(conic, low, high);
}
if conic.evaluate(d0)?.sub(conic.evaluate(d1)?).length() > tolerance {
return Err(
"offset_ruled_face: a multi-loop rim arc runs outside its OPEN conic's domain — \
refusing"
.into(),
);
}
let span = (d1 - d0).max(1e-12);
let head_is_empty = low - d0 <= 1e-9 * span;
let tail_is_empty = d1 - high <= 1e-9 * span;
match (head_is_empty, tail_is_empty) {
(true, false) => subcurve(conic, high, d1),
(false, true) => subcurve(conic, d0, low),
_ => Err(
"offset_ruled_face: a multi-loop rim arc crosses the rebuilt conic's period \
boundary with material on BOTH sides, so it is two pieces to join — deferred \
(refusing)"
.into(),
),
}
}
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),
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 && loop_record.coedges.len() != 1 {
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);
};
let lone_closed = edge_ids.len() == 1
&& edge_by_id
.get(&edge_ids[0])
.is_some_and(|edge| edge.start_vertex_id == edge.end_vertex_id);
if edge_ids.len() < 2 && !lone_closed {
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 lone_closed && pinned.is_empty() {
if debug { eprintln!("HOLE reject: lone closed edge {} is not pinned", edge_ids[0]); }
return Ok(None);
}
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);
census_push("seam_crossings", || {
crossing_note(
§ion.curve,
&plane,
curve_plane_crossing_near(§ion.curve, &plane, old, reach),
curve_plane_crossing_or_end_near(§ion.curve, &plane, old, reach, tolerance),
section.closed,
)
});
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 = if edge.start_vertex_id == edge.end_vertex_id {
arc_length_stations(edge, 5, true)?[1]
} else {
arc_length_stations(edge, 3, true)?[1]
};
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())
}