use super::*;
pub(super) fn build_octant_face(
center: Vec3,
normals: [Vec3; 3],
radius: f64,
t_ids: [u64; 3],
t_pts: [Vec3; 3],
arcs: &[(u64, u64, u64)],
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<(FaceRecord, EdgeRecord), KernelRefusal> {
let [m1, m2, m3] = normals;
if m1.cross(m2).dot(m3) < 0.5 {
return Err(KernelRefusal::internal(
KernelStage::Fragment,
"octant_frame",
"round_convex_corner: octant normals are not right-handed",
));
}
let half = std::f64::consts::FRAC_PI_2;
let meridian = crate::make_arc(center, m1, m3, radius, -half, half)
.or_refuse(KernelStage::Fragment, "make_arc")?;
let surface = crate::make_revolution(center, m3, &meridian, std::f64::consts::TAU)
.or_refuse(KernelStage::Fragment, "make_revolution")?;
let [t1, t2, t3] = t_ids;
let e_pole = next_id();
let pole_edge = EdgeRecord {
id: e_pole,
curve: crate::make_line(t_pts[2], t_pts[2]).or_refuse(KernelStage::Fragment, "make_line")?,
t0: 0.0,
t1: 1.0,
start_vertex_id: t3,
end_vertex_id: t3,
degenerate: true,
name: None,
};
let find = |from: u64, to: u64| -> Result<(u64, bool), KernelRefusal> {
for &(eid, s, e) in arcs {
if s == from && e == to {
return Ok((eid, true));
}
if s == to && e == from {
return Ok((eid, false));
}
}
Err(KernelRefusal::internal(
KernelStage::Fragment,
"tangent_arc",
format!("round_convex_corner: no tangent arc between vertices {from} and {to}"),
))
};
let (e12, f12) = find(t1, t2)?; let (e23, f23) = find(t2, t3)?; let (e31, f31) = find(t3, t1)?;
let pl = |u0, v0, u1, v1| {
crate::sweep_topology::parameter_line(u0, v0, u1, v1)
.or_refuse(KernelStage::Fragment, "parameter_line")
};
let loop_id = next_id();
let coedges = vec![
CoedgeRecord {
id: next_id(),
edge_id: e12,
forward: f12,
pcurve: pl(0.0, 0.5, 0.25, 0.5)?,
},
CoedgeRecord {
id: next_id(),
edge_id: e23,
forward: f23,
pcurve: pl(0.25, 0.5, 0.25, 1.0)?,
},
CoedgeRecord {
id: next_id(),
edge_id: e_pole,
forward: true,
pcurve: pl(0.25, 1.0, 0.0, 1.0)?,
},
CoedgeRecord {
id: next_id(),
edge_id: e31,
forward: f31,
pcurve: pl(0.0, 1.0, 0.0, 0.5)?,
},
];
let mid = surface
.evaluate(0.125, 0.75)
.or_refuse(KernelStage::Fragment, "evaluate")?;
let surface_normal = surface
.normal(0.125, 0.75)
.or_refuse(KernelStage::Fragment, "normal")?;
let outward = mid
.sub(center)
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let same_sense = surface_normal.dot(outward) > 0.0;
let face = FaceRecord {
id: next_id(),
surface,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
};
Ok((face, pole_edge))
}
pub(in crate::blend) fn build_general_corner_patch(
center: Vec3,
radius: f64,
normals: &[Vec3],
arc_edges: &[EdgeRecord],
outward: bool,
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<FaceRecord, KernelRefusal> {
use std::f64::consts::{FRAC_PI_2, PI, TAU};
let _patch_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch_ms");
let frame_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.frame_ms");
let n_faces = normals.len();
if n_faces < 3 || arc_edges.len() != n_faces {
return Err(KernelRefusal::input(
KernelStage::Collect,
"arc_count",
format!(
"build_general_corner_patch: expected N≥3 faces with one arc each, \
found {n_faces} faces and {} arcs",
arc_edges.len()
),
));
}
let pc = normals
.iter()
.fold(Vec3::default(), |acc, n| acc.add(*n))
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let e0 = pc
.perpendicular()
.or_refuse(KernelStage::Fragment, "perpendicular")?;
let e1 = pc
.cross(e0)
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let mut crit: Vec<Vec3> = normals.to_vec();
for i in 0..n_faces {
for j in (i + 1)..n_faces {
if let Ok(mid) = normals[i].add(normals[j]).normalized() {
crit.push(mid);
}
}
}
let mut best_phi = 0.0f64;
let mut best_max = f64::INFINITY;
for k in 0..360 {
let phi = PI * k as f64 / 360.0;
let polar = e0.scale(phi.cos()).add(e1.scale(phi.sin()));
let worst = crit
.iter()
.fold(0.0f64, |acc, d| acc.max(d.dot(polar).abs()));
if worst < best_max {
best_max = worst;
best_phi = phi;
}
}
let polar = e0
.scale(best_phi.cos())
.add(e1.scale(best_phi.sin()))
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
frame_timing.stop();
let surface_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.surface_ms");
let x_axis = pc.scale(-1.0);
let meridian = crate::make_arc(center, x_axis, polar, radius, -FRAC_PI_2, FRAC_PI_2)
.or_refuse(KernelStage::Fragment, "make_arc")?;
let surface = crate::make_revolution(center, polar, &meridian, TAU)
.or_refuse(KernelStage::Fragment, "make_revolution")?;
surface_timing.stop();
let mut order = vec![0usize];
let mut used = vec![false; n_faces];
used[0] = true;
let mut chain_vertex = arc_edges[0].start_vertex_id;
for _ in 0..(n_faces - 1) {
let next = (0..n_faces)
.find(|&j| !used[j] && arc_edges[j].end_vertex_id == chain_vertex)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Fragment,
"arc_chain",
"build_general_corner_patch: tangent arcs do not form a closed N-gon",
)
})?;
used[next] = true;
order.push(next);
chain_vertex = arc_edges[next].start_vertex_id;
}
if chain_vertex != arc_edges[0].end_vertex_id {
return Err(KernelRefusal::internal(
KernelStage::Fragment,
"arc_loop",
"build_general_corner_patch: tangent arc loop is not closed",
));
}
const PATCH_TRACK_SAMPLES: usize = 512;
const PATCH_TRACK_SAMPLES_LIMIT: usize = 4096;
let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
let mut coedges = Vec::with_capacity(n_faces);
for &ai in &order {
let track_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.track_ms");
let edge = &arc_edges[ai];
let at = |fraction: f64| edge.curve.evaluate(edge.t1 - (edge.t1 - edge.t0) * fraction);
let breaks = super::super::track_fit::curve_breaks(&edge.curve, edge.t0, edge.t1, false);
let fit = super::super::track_fit::fit_curve_track(
&surface,
&at,
&breaks,
floor,
PATCH_TRACK_SAMPLES,
PATCH_TRACK_SAMPLES_LIMIT,
)?;
if std::env::var("BREP_DEBUG_TRACK_FIT").is_ok() {
eprintln!(
"TRACK_FIT corner patch arc {}: miss {:.3e} at {} samples",
edge.id, fit.miss, fit.samples
);
}
if !fit.on_floor {
return Err(KernelRefusal::non_convergence(
KernelStage::Refine,
"patch_track",
format!(
"{} the corner patch's pcurve along arc {} misses its arc by {:.3e} at {} \
samples, against a floor of {floor:.1e}",
crate::blend::PCURVE_OFF_FLOOR,
edge.id,
fit.miss,
fit.samples
),
));
}
coedges.push(CoedgeRecord {
id: next_id(),
edge_id: edge.id,
forward: false,
pcurve: fit.curve,
});
track_timing.stop();
}
let _sense_timing = crate::blend::BlendTimer::start("timing.surgery.closure.patch.sense_ms");
let centre_projection =
crate::project_point_to_surface(&surface, center.add(pc.scale(radius)))
.or_refuse(KernelStage::Fragment, "project")?;
let surface_normal = surface
.normal(centre_projection.u, centre_projection.v)
.or_refuse(KernelStage::Fragment, "normal")?;
let same_sense = (surface_normal.dot(pc) > 0.0) == outward;
let loop_id = next_id();
Ok(FaceRecord {
id: next_id(),
surface,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
})
}
const EXACT_OCTANT_DEFAULT: bool = false;
thread_local! {
static EXACT_OCTANT_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(in crate::blend) fn exact_octant_on() -> bool {
if let Some(forced) = EXACT_OCTANT_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_EXACT_OCTANT").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => EXACT_OCTANT_DEFAULT,
}
}
pub(in crate::blend) fn build_corner_patch_exact(
center: Vec3,
radius: f64,
normals: &[Vec3],
arc_edges: &[EdgeRecord],
outward: bool,
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<(FaceRecord, Option<EdgeRecord>), KernelRefusal> {
if exact_octant_on() {
if let Some((face, pole)) = try_exact_octant(center, radius, normals, arc_edges, outward, name, next_id)? {
return Ok((face, Some(pole)));
}
}
Ok((build_general_corner_patch(center, radius, normals, arc_edges, outward, name, next_id)?, None))
}
fn try_exact_octant(
center: Vec3,
radius: f64,
normals: &[Vec3],
arc_edges: &[EdgeRecord],
outward: bool,
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<Option<(FaceRecord, EdgeRecord)>, KernelRefusal> {
if normals.len() != 3 || arc_edges.len() != 3 {
return Ok(None);
}
let orthogonal = (0..3).all(|i| ((i + 1)..3).all(|j| normals[i].dot(normals[j]).abs() <= 1e-12));
if !orthogonal || normals.iter().any(|n| (n.length() - 1.0).abs() > 1e-12) {
return Ok(None);
}
let mut m = [normals[0], normals[1], normals[2]];
if m[0].cross(m[1]).dot(m[2]) < 0.0 {
m.swap(1, 2);
}
let ideal: [Vec3; 3] = [center.add(m[0].scale(radius)), center.add(m[1].scale(radius)), center.add(m[2].scale(radius))];
let near = 1e-9 * (1.0 + radius);
let which = |point: Vec3| (0..3).find(|&k| point.sub(ideal[k]).length() <= near);
let mut triples: Vec<(u64, u64, u64)> = Vec::with_capacity(3);
let mut t_ids = [None::<u64>; 3];
let mut labels: Vec<(usize, usize)> = Vec::with_capacity(3);
for edge in arc_edges {
let start = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Fragment, "evaluate")?;
let end = edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Fragment, "evaluate")?;
let (Some(ks), Some(ke)) = (which(start), which(end)) else { return Ok(None) };
if ks == ke {
return Ok(None);
}
for (k, id) in [(ks, edge.start_vertex_id), (ke, edge.end_vertex_id)] {
match t_ids[k] {
None => t_ids[k] = Some(id),
Some(known) if known == id => {}
Some(_) => return Ok(None),
}
}
triples.push((edge.id, edge.start_vertex_id, edge.end_vertex_id));
labels.push((ks, ke));
}
let [Some(t1), Some(t2), Some(t3)] = t_ids else { return Ok(None) };
let mut pairs: Vec<(usize, usize)> = labels.iter().map(|&(a, b)| (a.min(b), a.max(b))).collect();
pairs.sort_unstable();
if pairs != vec![(0, 1), (0, 2), (1, 2)] {
return Ok(None);
}
let starts: Vec<usize> = labels.iter().map(|&(a, _)| a).collect();
if !(0..3).all(|k| starts.iter().filter(|&&a| a == k).count() == 1) {
return Ok(None);
}
let (mut face, pole) = build_octant_face(center, m, radius, [t1, t2, t3], ideal, &triples, name, next_id)?;
let arcs_forward = face.loops[0].coedges.iter().filter(|coedge| coedge.edge_id != pole.id).map(|coedge| coedge.forward).collect::<Vec<bool>>();
if arcs_forward.iter().any(|&forward| forward) {
if !arcs_forward.iter().all(|&forward| forward) {
return Ok(None);
}
let mut reversed = Vec::with_capacity(face.loops[0].coedges.len());
for coedge in face.loops[0].coedges.iter().rev() {
reversed.push(CoedgeRecord {
id: coedge.id,
edge_id: coedge.edge_id,
forward: if coedge.edge_id == pole.id { true } else { !coedge.forward },
pcurve: coedge.pcurve.reversed().or_refuse(KernelStage::Fragment, "reversed")?,
});
}
face.loops[0].coedges = reversed;
face.same_sense = !face.same_sense;
}
if !outward {
face.same_sense = !face.same_sense;
}
Ok(Some((face, pole)))
}
pub(in crate::blend) fn build_chamfer_corner_facet(
center: Vec3,
normals: &[Vec3],
chord_edges: &[EdgeRecord],
outward: bool,
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<FaceRecord, KernelRefusal> {
let n_faces = normals.len();
if n_faces < 3 || chord_edges.len() != n_faces {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chord_count",
format!(
"blend network: a chamfered star closes with the facet its end chords bound, which \
needs N≥3 faces carrying one chord each — this vertex has {n_faces} faces and {} \
sections",
chord_edges.len()
),
));
}
let pc = normals
.iter()
.fold(Vec3::default(), |acc, n| acc.add(*n))
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let mut order = vec![0usize];
let mut used = vec![false; n_faces];
used[0] = true;
let mut chain_vertex = chord_edges[0].start_vertex_id;
for _ in 0..(n_faces - 1) {
let next = (0..n_faces)
.find(|&j| !used[j] && chord_edges[j].end_vertex_id == chain_vertex)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Fragment,
"chord_chain",
"blend network: the chamfer facet's sections do not form a closed N-gon",
)
})?;
used[next] = true;
order.push(next);
chain_vertex = chord_edges[next].start_vertex_id;
}
if chain_vertex != chord_edges[0].end_vertex_id {
return Err(KernelRefusal::internal(
KernelStage::Fragment,
"chord_loop",
"blend network: the chamfer facet's section loop is not closed",
));
}
let corner_point = |index: usize| -> Result<Vec3, KernelRefusal> {
let edge = &chord_edges[index];
edge.curve
.evaluate(edge.t0)
.or_refuse(KernelStage::Fragment, "evaluate")
};
let points = order
.iter()
.map(|&index| corner_point(index))
.collect::<Result<Vec<_>, KernelRefusal>>()?;
let [a, b] = [points[0], points[1]];
let u_dir = b
.sub(a)
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let mut newell = Vec3::default();
for index in 0..n_faces {
let from = points[index];
let to = points[(index + 1) % n_faces];
newell = newell.add(from.sub(a).cross(to.sub(a)));
}
let normal = newell
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let v_dir = normal
.cross(u_dir)
.normalized()
.or_refuse(KernelStage::Fragment, "normalized")?;
let radius = points
.iter()
.fold(0.0f64, |worst, point| worst.max(point.sub(center).length()));
let flatness = 1e-9 * (1.0 + radius);
let skew = points
.iter()
.fold(0.0f64, |worst, point| worst.max(point.sub(a).dot(normal).abs()));
if skew > flatness {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"skew_ngon",
format!(
"blend network: the {n_faces} chords at a chamfered star bound a SKEW N-gon \
({skew:.3e} out of plane against {flatness:.3e}); a non-planar corner fill is not \
implemented"
),
));
}
let reach = points
.iter()
.fold(0.0f64, |worst, point| worst.max(point.sub(a).length()))
.max(1e-6)
* 4.0;
let origin = a.sub(u_dir.scale(reach)).sub(v_dir.scale(reach));
let plane = crate::make_plane(origin, u_dir, v_dir, 2.0 * reach, 2.0 * reach)
.or_refuse(KernelStage::Fragment, "make_plane")?;
let uv_of = |point: Vec3| -> Result<(f64, f64), KernelRefusal> {
let projection = crate::project_point_to_surface(&plane, point)
.or_refuse(KernelStage::Fragment, "project")?;
Ok((projection.u, projection.v))
};
let mut coedges = Vec::with_capacity(n_faces);
for &index in &order {
let edge = &chord_edges[index];
let (u0, v0) = uv_of(
edge.curve
.evaluate(edge.t1)
.or_refuse(KernelStage::Fragment, "evaluate")?,
)?;
let (u1, v1) = uv_of(
edge.curve
.evaluate(edge.t0)
.or_refuse(KernelStage::Fragment, "evaluate")?,
)?;
coedges.push(CoedgeRecord {
id: next_id(),
edge_id: edge.id,
forward: false,
pcurve: crate::sweep_topology::parameter_line(u0, v0, u1, v1)
.or_refuse(KernelStage::Fragment, "parameter_line")?,
});
}
let plane_normal = plane
.normal(0.5, 0.5)
.or_refuse(KernelStage::Fragment, "normal")?;
let same_sense = (plane_normal.dot(pc) > 0.0) == outward;
let loop_id = next_id();
Ok(FaceRecord {
id: next_id(),
surface: plane,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
})
}