use crate::KernelRefusal;
use crate::topology::BrepSolid;
use crate::{solid_scale, AnalyticSurface, KernelTolerances, Vec3};
use rustc_hash::FxHashMap as HashMap;
const HEAL_K: f64 = 1e-5;
const ACTIVATION_FLOOR: f64 = 1e-9;
const FEATURE_CAP: f64 = 0.25;
const PLANE_ANCHOR: f64 = 1e-6;
fn visit_heal_pairs(points: &[Vec3], heal_tol: f64, mut visit: impl FnMut(usize, usize)) {
let brute_force = |visit: &mut dyn FnMut(usize, usize)| {
for i in 0..points.len() {
for j in i + 1..points.len() {
visit(i, j);
}
}
};
let width = 2.0 * heal_tol;
if points.len() <= 32 || heal_tol < 1e-150 || !width.is_finite() || width <= 0.0 {
brute_force(&mut visit);
return;
}
let keys: Option<Vec<[i64; 3]>> = points
.iter()
.map(|point| {
let q = [point.x / width, point.y / width, point.z / width];
q.iter()
.all(|v| v.is_finite() && v.abs() <= (1u64 << 48) as f64)
.then(|| q.map(|v| v.floor() as i64))
})
.collect();
let Some(keys) = keys else {
brute_force(&mut visit);
return;
};
let mut cells: HashMap<[i64; 3], Vec<usize>> = HashMap::default();
for (i, key) in keys.iter().enumerate() {
cells.entry(*key).or_default().push(i);
}
let mut candidates = Vec::new();
for (i, &[x, y, z]) in keys.iter().enumerate() {
candidates.clear();
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if let Some(indices) = cells.get(&[x + dx, y + dy, z + dz]) {
candidates.extend(indices.iter().copied().filter(|&j| j > i));
}
}
}
}
candidates.sort_unstable();
for &j in &candidates {
visit(i, j);
}
}
}
pub(crate) fn heal_operands(
solid: &mut BrepSolid,
policy: &KernelTolerances,
) -> Result<(), KernelRefusal> {
if solid.vertices.len() < 2 {
return Ok(());
}
let diagonal = solid_scale(solid);
let heal_tol = policy.heal_band(diagonal, HEAL_K);
if !(heal_tol > 0.0) || !heal_tol.is_finite() {
return Ok(());
}
let original = solid.clone();
let moved = heal_operands_inner(solid, heal_tol, policy.model)?;
let debug = std::env::var("BREP_DEBUG_BOOL").is_ok();
if !moved {
if debug {
eprintln!("heal: no-op (0 vertices moved, heal_tol={heal_tol:.3e})");
}
return Ok(());
}
let before = original.validate_with_tolerances(policy).len();
if debug {
let count = original
.vertices
.iter()
.zip(&solid.vertices)
.filter(|(a, b)| a.point.sub(b.point).length() > 0.0)
.count();
eprintln!("heal: moved {count} vertices (heal_tol={heal_tol:.3e})");
}
let after = solid.validate_with_tolerances(policy).len();
if after > before {
if debug {
eprintln!("heal: discarded (validate worsened {before} -> {after})");
}
*solid = original;
return Ok(());
}
if debug {
oracle_scan(solid, heal_tol);
}
Ok(())
}
fn heal_operands_inner(solid: &mut BrepSolid, heal_tol: f64, model: f64) -> Result<bool, KernelRefusal> {
let n = solid.vertices.len();
let planes_by_vertex = incident_planes(solid);
let shortest_edge = shortest_incident_edges(solid);
let ids: Vec<u64> = solid.vertices.iter().map(|v| v.id).collect();
let points: Vec<Vec3> = solid.vertices.iter().map(|v| v.point).collect();
let index_of: HashMap<u64, usize> = ids.iter().enumerate().map(|(i, id)| (*id, i)).collect();
let mut forbidden: rustc_hash::FxHashSet<(usize, usize)> = rustc_hash::FxHashSet::default();
for edge in &solid.edges {
if edge.start_vertex_id == edge.end_vertex_id {
continue;
}
let (Some(&a), Some(&b)) = (
index_of.get(&edge.start_vertex_id),
index_of.get(&edge.end_vertex_id),
) else {
continue;
};
let length = points[a].sub(points[b]).length();
if !edge.degenerate && length > heal_tol {
forbidden.insert((a.min(b), a.max(b)));
}
}
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut Vec<usize>, mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
let caps: Vec<f64> = ids
.iter()
.map(|id| {
shortest_edge
.get(id)
.map(|len| len * FEATURE_CAP)
.unwrap_or(f64::INFINITY)
})
.collect();
visit_heal_pairs(&points, heal_tol, |i, j| {
if forbidden.contains(&(i, j)) {
return;
}
let radius = heal_tol.min(caps[i]).min(caps[j]);
if points[i].sub(points[j]).length() <= radius {
let a = find(&mut parent, i);
let b = find(&mut parent, j);
if a != b {
parent[a] = b;
}
}
});
let mut clusters: HashMap<usize, Vec<usize>> = HashMap::default();
for i in 0..n {
let root = find(&mut parent, i);
clusters.entry(root).or_default().push(i);
}
let mut new_point: Vec<Vec3> = points.clone();
let mut any_move = false;
let mut measured = crate::EntityTolerances::with_floor(solid, model);
for members in clusters.values() {
let base = members
.iter()
.fold(Vec3::default(), |acc, &m| acc.add(points[m]))
.scale(1.0 / members.len() as f64);
let mut planes: Vec<(Vec3, f64)> = Vec::new();
for &m in members {
if let Some(list) = planes_by_vertex.get(&ids[m]) {
for &(normal, offset) in list {
if !planes.iter().any(|(pn, pd)| {
pn.dot(normal).abs() > 1.0 - 1e-9 && (pd - offset).abs() <= heal_tol
}) {
planes.push((normal, offset));
}
}
}
}
let mut target = plane_snap(base, &planes);
let is_fuse = members.len() > 1;
if is_fuse {
let budgets: Vec<f64> = members.iter().map(|&m| {
let plane_error = planes_by_vertex.get(&ids[m]).into_iter()
.flatten().map(|(normal, offset)| (normal.dot(points[m]) - offset).abs())
.fold(0.0_f64, f64::max);
measured.vertex(ids[m]).max(plane_error).max(model).min(heal_tol)
}).collect();
let fits = |candidate: Vec3| members.iter().zip(&budgets)
.all(|(&m, &budget)| candidate.sub(points[m]).length() <= budget);
if !fits(target) {
let mut anchors: Vec<usize> = (0..members.len()).collect();
anchors.sort_by(|&a, &b| budgets[a].total_cmp(&budgets[b])
.then_with(|| ids[members[a]].cmp(&ids[members[b]])));
let replacement = anchors.into_iter()
.map(|index| plane_snap(points[members[index]], &planes))
.find(|&candidate| fits(candidate));
let Some(replacement) = replacement else { continue; };
target = replacement;
}
}
let off_plane = planes
.iter()
.map(|(nrm, off)| (nrm.dot(base) - off).abs())
.fold(0.0_f64, f64::max);
if !is_fuse && off_plane <= ACTIVATION_FLOOR {
continue;
}
for &m in members {
if target.sub(points[m]).length() > heal_tol {
if is_fuse && base.sub(points[m]).length() <= heal_tol {
if base.sub(points[m]).length() > 0.0 {
new_point[m] = base;
any_move = true;
}
}
continue;
}
if target.sub(points[m]).length() > 0.0 {
new_point[m] = target;
any_move = true;
}
}
}
if !any_move {
return Ok(false);
}
for (i, vertex) in solid.vertices.iter_mut().enumerate() {
vertex.point = new_point[i];
}
crate::boolean::commit_nearby_edge_endpoints(solid, heal_tol)?;
Ok(true)
}
fn incident_planes(solid: &BrepSolid) -> HashMap<u64, Vec<(Vec3, f64)>> {
let edge_vertices: HashMap<u64, (u64, u64)> = solid
.edges
.iter()
.map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id)))
.collect();
let mut out: HashMap<u64, Vec<(Vec3, f64)>> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
let Some(AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
..
}) = face.surface.analytic()
else {
continue;
};
let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
continue;
};
let offset = normal.dot(*origin);
let mut touched: rustc_hash::FxHashSet<u64> = rustc_hash::FxHashSet::default();
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
if let Some(&(start, end)) = edge_vertices.get(&coedge.edge_id) {
touched.insert(start);
touched.insert(end);
}
}
}
for vertex_id in touched {
out.entry(vertex_id).or_default().push((normal, offset));
}
}
out
}
fn shortest_incident_edges(solid: &BrepSolid) -> HashMap<u64, f64> {
let points: HashMap<u64, Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
let mut out: HashMap<u64, f64> = HashMap::default();
for edge in &solid.edges {
if edge.degenerate || edge.start_vertex_id == edge.end_vertex_id {
continue;
}
let (Some(&a), Some(&b)) = (
points.get(&edge.start_vertex_id),
points.get(&edge.end_vertex_id),
) else {
continue;
};
let length = a.sub(b).length();
for id in [edge.start_vertex_id, edge.end_vertex_id] {
let slot = out.entry(id).or_insert(f64::INFINITY);
if length < *slot {
*slot = length;
}
}
}
out
}
fn plane_snap(base: Vec3, planes: &[(Vec3, f64)]) -> Vec3 {
if planes.is_empty() {
return base;
}
let mut matrix = [[0.0f64; 3]; 3];
let mut rhs = [0.0f64; 3];
for i in 0..3 {
matrix[i][i] = PLANE_ANCHOR;
}
let base_arr = [base.x, base.y, base.z];
for i in 0..3 {
rhs[i] = PLANE_ANCHOR * base_arr[i];
}
for (normal, offset) in planes {
let na = [normal.x, normal.y, normal.z];
for i in 0..3 {
for j in 0..3 {
matrix[i][j] += na[i] * na[j];
}
rhs[i] += offset * na[i];
}
}
match crate::fit::solve_small(matrix, rhs, 3) {
Ok(solution) => Vec3::new(solution[0], solution[1], solution[2]),
Err(_) => base,
}
}
fn oracle_scan(solid: &BrepSolid, heal_tol: f64) {
let vs = &solid.vertices;
for i in 0..vs.len() {
for j in (i + 1)..vs.len() {
let gap = vs[i].point.sub(vs[j].point).length();
if gap <= heal_tol {
eprintln!(
"heal oracle: vertices {} and {} still within heal_tol ({:.3e})",
vs[i].id, vs[j].id, gap
);
}
}
}
}
const TRIM_REFIT_DETECT_INTERIOR: usize = 16;
const TRIM_REFIT_REREAD_INTERIOR: usize = 64;
const TRIM_REFIT_SCOPE_INTERIOR: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TrimRefitDecline {
Unreadable,
EdgeOffCarrier,
CurvedExtension,
CornerExtension,
BeyondNativeReach,
ClosedCarrier,
OldEndWrong,
RefitFailed,
RefitOffItsBar,
RereadOverBar,
EndMoved,
UvJointWorsened,
JointWorsened,
MateUnrepaired,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TrimRefitRollback {
NewOrChangedIssue,
VectorAreaWorsened,
VectorAreaUnreadable,
VectorAreaReadingChanged,
VectorAreaUnmeasured,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TrimRefitDecision {
Refit,
Declined(TrimRefitDecline),
RolledBack(TrimRefitRollback),
}
#[derive(Clone, Debug)]
pub(crate) struct TrimRefitEntry {
pub face: u64,
pub coedge: u64,
pub edge: u64,
pub miss: f64,
pub standoff: Vec<(u64, f64)>,
pub reread: Option<f64>,
pub outside_domain: usize,
pub witnessed: usize,
pub old_bits: u64,
pub new_bits: Option<u64>,
pub decision: TrimRefitDecision,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct TrimRefitReport {
pub coedges_read: usize,
pub entries: Vec<TrimRefitEntry>,
pub seconds: TrimRefitSeconds,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct TrimRefitSeconds {
pub detect: f64,
pub scope: f64,
pub construct: f64,
pub gate: f64,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ScopeProfile {
pub candidacy_feet: usize,
pub old_claim_feet: usize,
pub final_claim_feet: usize,
pub projector_calls: usize,
pub projector_seconds: f64,
pub newton_iterations: usize,
pub grid_fallbacks: usize,
pub witness_evaluations: usize,
pub final_claims_reused: usize,
pub final_claims_read: usize,
pub replayed_feet: usize,
pub repeated_feet: [usize; 3],
pub sampled: usize,
pub hint_agrees: usize,
pub hint_smaller: usize,
pub hint_larger: usize,
pub hint_unreadable: usize,
keys: rustc_hash::FxHashSet<(usize, u64, u64)>,
phase: ScopePhase,
sample_clock: usize,
}
const SCOPE_PROFILE_SAMPLE: usize = 64;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum ScopePhase {
#[default]
Candidacy,
OldClaim,
FinalClaim,
}
impl ScopeProfile {
fn summary(&self) -> String {
format!(
"final claims reused {} read {}; replayed feet {}; repeated feet {:?}; feet candidacy {} old-claim {} final-claim {} (distinct keys {}); scope projector calls {} in {:.3}s; newton iterations {}; grid fallbacks {}; witnesses {}; hint-only sample {}: agrees {} smaller {} larger {} unreadable {}",
self.final_claims_reused, self.final_claims_read, self.replayed_feet, self.repeated_feet,
self.candidacy_feet, self.old_claim_feet, self.final_claim_feet, self.keys.len(),
self.projector_calls, self.projector_seconds, self.newton_iterations, self.grid_fallbacks,
self.witness_evaluations, self.sampled, self.hint_agrees, self.hint_smaller, self.hint_larger,
self.hint_unreadable
)
}
}
thread_local! {
static SCOPE_PROFILE: std::cell::RefCell<Option<ScopeProfile>> = const { std::cell::RefCell::new(None) };
}
fn profiling(hook: impl FnOnce(&mut ScopeProfile)) {
SCOPE_PROFILE.with(|cell| {
if let Some(profile) = cell.borrow_mut().as_mut() {
hook(profile);
}
});
}
fn profiling_on() -> bool {
SCOPE_PROFILE.with(|cell| cell.borrow().is_some())
}
fn trim_refit_profiling() -> bool {
std::env::var_os("BREP_DEBUG_TRIM_REFIT_PROFILE").is_some()
}
pub(crate) fn pcurve_bits(curve: &crate::NurbsCurve) -> u64 {
use std::hash::Hasher;
let mut hasher = rustc_hash::FxHasher::default();
hasher.write_usize(curve.degree);
for knot in &curve.knots {
hasher.write_u64(knot.to_bits());
}
for point in &curve.control_points {
for value in [point.x, point.y, point.z, point.w] {
hasher.write_u64(value.to_bits());
}
}
hasher.finish()
}
fn trim_refit_tracing() -> Option<bool> {
std::env::var("BREP_DEBUG_TRIM_REFIT").ok().map(|value| value == "all")
}
pub(crate) fn refit_inherited_trims(
solid: &mut BrepSolid,
policy: &KernelTolerances,
) -> Result<TrimRefitReport, KernelRefusal> {
if std::env::var_os("BREP_DEBUG_TRIM_REFIT_OFF").is_some() {
return Ok(TrimRefitReport::default());
}
refit_inherited_trims_with(solid, policy, |before, after| trim_refit_gate(before, after, policy))
}
fn refit_inherited_trims_with(
solid: &mut BrepSolid,
policy: &KernelTolerances,
mut gate: impl FnMut(&BrepSolid, &BrepSolid) -> Option<TrimRefitRollback>,
) -> Result<TrimRefitReport, KernelRefusal> {
let trace = trim_refit_tracing();
let owns_profile = trim_refit_profiling() && !profiling_on();
if owns_profile {
SCOPE_PROFILE.with(|cell| *cell.borrow_mut() = Some(ScopeProfile::default()));
}
let floor = crate::PCURVE_REFINEMENT_TOLERANCE;
let edges: HashMap<u64, &crate::topology::EdgeRecord> =
solid.edges.iter().map(|edge| (edge.id, edge)).collect();
let mut uses_of: HashMap<u64, Vec<CoedgeIndex>> = HashMap::default();
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
for (loop_index, lp) in face.loops.iter().enumerate() {
for (coedge_index, coedge) in lp.coedges.iter().enumerate() {
uses_of.entry(coedge.edge_id).or_default().push((shell_index, face_index, loop_index, coedge_index));
}
}
}
}
let mut report = TrimRefitReport::default();
let mut claims: HashMap<u64, (EdgeClaim, Option<TrimRefitDecline>)> = HashMap::default();
let replay_on = std::env::var_os("BREP_DEBUG_TRIM_REFIT_NO_REPLAY").is_none();
let mut records: HashMap<u64, ClaimRecord> = HashMap::default();
let mut replacements: Vec<(CoedgeIndex, crate::NurbsCurve, usize)> = Vec::new();
let mut working: Option<BrepSolid> = None;
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
for (loop_index, lp) in face.loops.iter().enumerate() {
for (coedge_index, coedge) in lp.coedges.iter().enumerate() {
report.coedges_read += 1;
let Some(&edge) = edges.get(&coedge.edge_id) else {
report.entries.push(TrimRefitEntry {
face: face.id,
coedge: coedge.id,
edge: coedge.edge_id,
miss: f64::NAN,
standoff: Vec::new(),
reread: None,
outside_domain: 0,
witnessed: 0,
old_bits: pcurve_bits(&coedge.pcurve),
new_bits: None,
decision: TrimRefitDecision::Declined(TrimRefitDecline::Unreadable),
});
continue;
};
if edge.degenerate {
continue;
}
let old_bits = pcurve_bits(&coedge.pcurve);
if trace == Some(true) {
eprintln!(
"TRIM-REFIT-ENTRY face {} coedge {} edge {} bits {old_bits:016x}",
face.id, coedge.id, edge.id
);
}
let mut entry = TrimRefitEntry {
face: face.id,
coedge: coedge.id,
edge: edge.id,
miss: f64::NAN,
standoff: Vec::new(),
reread: None,
outside_domain: 0,
witnessed: 0,
old_bits,
new_bits: None,
decision: TrimRefitDecision::Declined(TrimRefitDecline::Unreadable),
};
let started = web_time::Instant::now();
let miss = crate::reading_memo::memoized(
crate::reading_memo::Reading::RefitDetection,
crate::mass_properties::coedge_use_identity(&face.surface, &coedge.pcurve, edge, coedge.forward),
|| {
detection_stations(&coedge.pcurve, edge, coedge.forward)
.and_then(|stations| worst_same_parameter(&face.surface, &coedge.pcurve, edge, coedge.forward, &stations))
},
);
report.seconds.detect += started.elapsed().as_secs_f64();
let Some(miss) = miss else {
report.entries.push(entry);
continue;
};
entry.miss = miss;
if miss <= floor {
continue;
}
profiling(|profile| profile.phase = ScopePhase::Candidacy);
let started = web_time::Instant::now();
let own = edge_standoff_read(
&face.surface,
edge,
coedge.forward,
scope_stations(&[&coedge.pcurve], edge, coedge.forward, TRIM_REFIT_DETECT_INTERIOR).as_deref(),
policy.model,
None,
None,
Some(miss),
);
report.seconds.scope += started.elapsed().as_secs_f64();
let Some(own) = own.map(|read| read.worst) else {
report.entries.push(entry);
continue;
};
if miss <= floor + own {
continue;
}
match face.surface.closed_directions() {
Ok((closed_u, closed_v)) if closed_u || closed_v => {
entry.decision = TrimRefitDecision::Declined(TrimRefitDecline::ClosedCarrier);
report.entries.push(entry);
continue;
}
Err(_) => {
entry.decision = TrimRefitDecision::Declined(TrimRefitDecline::Unreadable);
report.entries.push(entry);
continue;
}
Ok(_) => {}
}
let uses = uses_of.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
if !claims.contains_key(&edge.id) {
profiling(|profile| profile.phase = ScopePhase::OldClaim);
let started = web_time::Instant::now();
let old_trims = trims_of(solid, uses);
let (claim, record) = edge_claim_with(solid, uses, edge, &old_trims, policy.model, None, replay_on);
report.seconds.scope += started.elapsed().as_secs_f64();
claims.insert(edge.id, claim);
if replay_on {
records.insert(edge.id, record);
}
}
let claim = &claims[&edge.id];
let current = working.get_or_insert_with(|| solid.clone());
let at = (shell_index, face_index, loop_index, coedge_index);
match refit_one(current, at, edge, claim, policy, &mut entry, &mut report.seconds) {
Ok(pcurve) => {
entry.new_bits = Some(pcurve_bits(&pcurve));
entry.decision = TrimRefitDecision::Refit;
current.shells[shell_index].faces[face_index].loops[loop_index].coedges[coedge_index].pcurve = pcurve.clone();
replacements.push((at, pcurve, report.entries.len()));
}
Err(decline) => entry.decision = TrimRefitDecision::Declined(decline),
}
report.entries.push(entry);
}
}
}
}
profiling(|profile| profile.phase = ScopePhase::FinalClaim);
let started = web_time::Instant::now();
let mut refit_edges: Vec<u64> = replacements.iter().map(|(_, _, index)| report.entries[*index].edge).collect();
refit_edges.sort_unstable();
refit_edges.dedup();
records.retain(|edge, _| refit_edges.binary_search(edge).is_ok());
for edge_id in refit_edges {
let Some(&edge) = edges.get(&edge_id) else { continue };
let uses = uses_of.get(&edge_id).map(Vec::as_slice).unwrap_or(&[]);
let (claim, decline) = {
let old_trims = trims_of(solid, uses);
let mut trims = old_trims.clone();
for ((s, f, l, c), pcurve, index) in &replacements {
if report.entries[*index].edge == edge_id {
trims.push((pcurve, solid.shells[*s].faces[*f].loops[*l].coedges[*c].forward));
}
}
final_claim(solid, uses, edge, &old_trims, &trims, claims.get(&edge_id), records.get(&edge_id), policy.model).0
};
let on_edge: Vec<(CoedgeIndex, usize)> = replacements
.iter()
.filter(|(_, _, index)| report.entries[*index].edge == edge_id)
.map(|(at, _, index)| (*at, *index))
.collect();
for &((s, f, l, c), index) in &on_edge {
report.entries[index].outside_domain += claim.outside;
report.entries[index].witnessed += claim.witnessed;
if let Some(decline) = decline {
report.entries[index].decision = TrimRefitDecision::Declined(decline);
if let Some(current) = working.as_mut() {
current.shells[s].faces[f].loops[l].coedges[c].pcurve = solid.shells[s].faces[f].loops[l].coedges[c].pcurve.clone();
}
}
}
}
report.seconds.scope += started.elapsed().as_secs_f64();
if let Some(current) = working.as_mut() {
settle_refits(solid, current, &replacements, &mut report.entries);
}
replacements.retain(|(_, _, index)| report.entries[*index].decision == TrimRefitDecision::Refit);
drop(edges);
if let Some(after) = working.filter(|_| !replacements.is_empty()) {
let started = web_time::Instant::now();
let verdict = gate(&*solid, &after);
report.seconds.gate += started.elapsed().as_secs_f64();
match verdict {
None => {
for ((s, f, l, c), pcurve, _) in replacements {
solid.shells[s].faces[f].loops[l].coedges[c].pcurve = pcurve;
}
}
Some(rollback) => {
for (.., entry_index) in replacements {
report.entries[entry_index].decision = TrimRefitDecision::RolledBack(rollback);
}
}
}
}
if trace.is_some() {
for entry in &report.entries {
eprintln!(
"TRIM-REFIT face {} coedge {} edge {} miss {:.4e} standoff {:?} outside {} witnessed {} reread {:?} bits {:016x} -> {:?} {:?}",
entry.face, entry.coedge, entry.edge, entry.miss, entry.standoff, entry.outside_domain, entry.witnessed, entry.reread,
entry.old_bits, entry.new_bits.map(|bits| format!("{bits:016x}")), entry.decision
);
}
eprintln!("TRIM-REFIT-SECONDS read {} {:?}", report.coedges_read, report.seconds);
}
if owns_profile {
if let Some(profile) = SCOPE_PROFILE.with(|cell| cell.borrow_mut().take()) {
eprintln!("TRIM-REFIT-SCOPE-PROFILE {}", profile.summary());
}
}
Ok(report)
}
type CoedgeIndex = (usize, usize, usize, usize);
fn trims_of<'a>(solid: &'a BrepSolid, uses: &[CoedgeIndex]) -> Vec<(&'a crate::NurbsCurve, bool)> {
uses.iter()
.map(|&(s, f, l, c)| {
let coedge = &solid.shells[s].faces[f].loops[l].coedges[c];
(&coedge.pcurve, coedge.forward)
})
.collect()
}
#[derive(Clone, Debug, Default)]
struct EdgeClaim {
standoff: Vec<(u64, f64)>,
outside: usize,
witnessed: usize,
}
#[cfg_attr(not(test), allow(dead_code))]
fn edge_claim(
solid: &BrepSolid,
uses: &[CoedgeIndex],
edge: &crate::topology::EdgeRecord,
trims: &[(&crate::NurbsCurve, bool)],
model: f64,
) -> (EdgeClaim, Option<TrimRefitDecline>) {
edge_claim_with(solid, uses, edge, trims, model, None, false).0
}
fn edge_claim_with(
solid: &BrepSolid,
uses: &[CoedgeIndex],
edge: &crate::topology::EdgeRecord,
trims: &[(&crate::NurbsCurve, bool)],
model: f64,
replay: Option<&ClaimRecord>,
record: bool,
) -> ((EdgeClaim, Option<TrimRefitDecline>), ClaimRecord) {
let mut records = ClaimRecord::new();
let mut claim = EdgeClaim::default();
let Some(stations) = edge_claim_stations(edge, trims) else {
return ((claim, Some(TrimRefitDecline::Unreadable)), records);
};
let mut decline = None;
let mut faces: Vec<(usize, usize)> = uses.iter().map(|&(s, f, _, _)| (s, f)).collect();
faces.sort_unstable();
faces.dedup();
for (s, f) in faces {
let carrier = &solid.shells[s].faces[f];
let lookup = replay.and_then(|replay| replay.iter().find(|(at, _)| *at == (s, f))).map(|(_, feet)| station_replay(feet));
let mut own = record.then(StationRecord::new);
let read = edge_standoff_replay(&carrier.surface, edge, true, Some(stations.as_slice()), model, lookup.as_ref(), own.as_mut());
if let Some(own) = own {
records.push(((s, f), own));
}
let Some(read) = read else {
return ((claim, Some(TrimRefitDecline::Unreadable)), records);
};
claim.standoff.push((carrier.id, read.worst));
claim.outside += read.outside;
claim.witnessed += read.witnessed;
if decline.is_none() {
decline = match read.extension {
Some(kind) => Some(kind),
None if read.worst > model => Some(TrimRefitDecline::EdgeOffCarrier),
None => None,
};
}
}
((claim, decline), records)
}
fn edge_claim_stations(edge: &crate::topology::EdgeRecord, trims: &[(&crate::NurbsCurve, bool)]) -> Option<Vec<f64>> {
let mut sets = vec![edge_breaks(edge, true)?];
for &(trim, forward) in trims {
let breaks = pcurve_breaks(trim)?;
sets.push(if forward { breaks } else { breaks.into_iter().map(|fraction| 1.0 - fraction).collect() });
}
Some(span_stations(&merged_breaks(&sets)?, TRIM_REFIT_SCOPE_INTERIOR))
}
fn settle_refits(
solid: &BrepSolid,
working: &mut BrepSolid,
replacements: &[(CoedgeIndex, crate::NurbsCurve, usize)],
entries: &mut [TrimRefitEntry],
) {
loop {
let unrepaired: rustc_hash::FxHashSet<u64> = entries
.iter()
.filter(|entry| entry.decision != TrimRefitDecision::Refit)
.map(|entry| entry.edge)
.collect();
let mut changed = false;
for &((s, f, l, c), _, index) in replacements {
if entries[index].decision != TrimRefitDecision::Refit {
continue;
}
let decline = if unrepaired.contains(&entries[index].edge) {
Some(TrimRefitDecline::MateUnrepaired)
} else {
joints_not_worse(solid, working, (s, f, l, c)).err()
};
if let Some(decline) = decline {
entries[index].decision = TrimRefitDecision::Declined(decline);
working.shells[s].faces[f].loops[l].coedges[c].pcurve = solid.shells[s].faces[f].loops[l].coedges[c].pcurve.clone();
changed = true;
}
}
if !changed {
return;
}
}
}
fn joints_not_worse(solid: &BrepSolid, working: &BrepSolid, (s, f, l, c): CoedgeIndex) -> Result<(), TrimRefitDecline> {
let surface = &solid.shells[s].faces[f].surface;
let (old_loop, new_loop) = (&solid.shells[s].faces[f].loops[l], &working.shells[s].faces[f].loops[l]);
let count = old_loop.coedges.len();
let joints: Vec<(usize, f64, f64)> =
if count == 1 { vec![(c, 1.0, 0.0)] } else { vec![((c + count - 1) % count, 1.0, 0.0), ((c + 1) % count, 0.0, 1.0)] };
let gap = |a: [f64; 2], b: [f64; 2]| ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt();
let point = |uv: [f64; 2]| surface.evaluate_extended(uv[0], uv[1]).map_err(|_| TrimRefitDecline::Unreadable);
for (theirs, their_fraction, own_fraction) in joints {
let ends = |lp: &crate::topology::LoopRecord| -> Result<([f64; 2], [f64; 2]), TrimRefitDecline> {
Ok((
uv_at(&lp.coedges[theirs].pcurve, their_fraction).ok_or(TrimRefitDecline::Unreadable)?,
uv_at(&lp.coedges[c].pcurve, own_fraction).ok_or(TrimRefitDecline::Unreadable)?,
))
};
let ((old_theirs, old_own), (new_theirs, new_own)) = (ends(old_loop)?, ends(new_loop)?);
let (before, after) = (gap(old_theirs, old_own), gap(new_theirs, new_own));
if !(before.is_finite() && after.is_finite()) {
return Err(TrimRefitDecline::Unreadable);
}
if after > before {
return Err(TrimRefitDecline::UvJointWorsened);
}
let (before, after) = (point(old_theirs)?.sub(point(old_own)?).length(), point(new_theirs)?.sub(point(new_own)?).length());
if !(before.is_finite() && after.is_finite()) {
return Err(TrimRefitDecline::Unreadable);
}
if after > before {
return Err(TrimRefitDecline::JointWorsened);
}
}
Ok(())
}
fn final_claim(
solid: &BrepSolid,
uses: &[CoedgeIndex],
edge: &crate::topology::EdgeRecord,
old_trims: &[(&crate::NurbsCurve, bool)],
trims: &[(&crate::NurbsCurve, bool)],
old: Option<&(EdgeClaim, Option<TrimRefitDecline>)>,
record: Option<&ClaimRecord>,
model: f64,
) -> ((EdgeClaim, Option<TrimRefitDecline>), bool) {
let same_stations = match (edge_claim_stations(edge, old_trims), edge_claim_stations(edge, trims)) {
(Some(old), Some(new)) => old.len() == new.len() && old.iter().zip(&new).all(|(a, b)| a.to_bits() == b.to_bits()),
_ => false,
};
match old.filter(|_| same_stations) {
Some(old) => {
profiling(|profile| profile.final_claims_reused += 1);
(old.clone(), true)
}
None => {
profiling(|profile| profile.final_claims_read += 1);
(edge_claim_with(solid, uses, edge, trims, model, record, false).0, false)
}
}
}
fn refit_one(
solid: &BrepSolid,
(shell_index, face_index, loop_index, coedge_index): CoedgeIndex,
edge: &crate::topology::EdgeRecord,
(claim, claim_decline): &(EdgeClaim, Option<TrimRefitDecline>),
policy: &KernelTolerances,
entry: &mut TrimRefitEntry,
seconds: &mut TrimRefitSeconds,
) -> Result<crate::NurbsCurve, TrimRefitDecline> {
let floor = crate::PCURVE_REFINEMENT_TOLERANCE;
let face = &solid.shells[shell_index].faces[face_index];
let lp = &face.loops[loop_index];
let coedge = &lp.coedges[coedge_index];
let surface = &face.surface;
let (closed_u, closed_v) = surface.closed_directions().map_err(|_| TrimRefitDecline::Unreadable)?;
if closed_u || closed_v {
return Err(TrimRefitDecline::ClosedCarrier);
}
entry.standoff = claim.standoff.clone();
entry.outside_domain += claim.outside;
entry.witnessed += claim.witnessed;
if let Some(decline) = claim_decline {
return Err(*decline);
}
if claim.standoff.iter().any(|&(_, standoff)| !(standoff <= policy.model)) {
return Err(TrimRefitDecline::EdgeOffCarrier);
}
let own = claim
.standoff
.iter()
.find(|&&(id, _)| id == face.id)
.map(|&(_, standoff)| standoff)
.ok_or(TrimRefitDecline::Unreadable)?;
let bar = floor + own;
let old_start = coedge_points(surface, &coedge.pcurve, edge, coedge.forward, 0.0).ok_or(TrimRefitDecline::Unreadable)?;
let old_end = coedge_points(surface, &coedge.pcurve, edge, coedge.forward, 1.0).ok_or(TrimRefitDecline::Unreadable)?;
if old_start.0.sub(old_start.1).length() > bar || old_end.0.sub(old_end.1).length() > bar {
return Err(TrimRefitDecline::OldEndWrong);
}
let started = web_time::Instant::now();
let result = construct_refit(surface, coedge, edge, floor).and_then(|pcurve| {
accept_refit(surface, lp, coedge, coedge_index, edge, pcurve, bar, (old_start, old_end), entry)
});
seconds.construct += started.elapsed().as_secs_f64();
result
}
fn construct_refit(
surface: &crate::NurbsSurface,
coedge: &crate::topology::CoedgeRecord,
edge: &crate::topology::EdgeRecord,
floor: f64,
) -> Result<crate::NurbsCurve, TrimRefitDecline> {
let fit = crate::fit_pcurve_on_surface_range(surface, &edge.curve, edge.t0, edge.t1, coedge.forward, floor)
.map_err(|_| TrimRefitDecline::RefitFailed)?;
if !fit.report.on_bar() {
return Err(TrimRefitDecline::RefitOffItsBar);
}
let mut pcurve = fit.curve;
let degree = pcurve.degree;
let knots = &pcurve.knots;
let clamped = knots.len() >= 2 * (degree + 1)
&& knots[..=degree].iter().all(|&knot| knot == knots[0])
&& knots[knots.len() - 1 - degree..].iter().all(|&knot| knot == knots[knots.len() - 1]);
if !clamped || pcurve.control_points.len() < 2 {
return Err(TrimRefitDecline::RefitFailed);
}
let old_start_uv = uv_at(&coedge.pcurve, 0.0).ok_or(TrimRefitDecline::Unreadable)?;
let old_end_uv = uv_at(&coedge.pcurve, 1.0).ok_or(TrimRefitDecline::Unreadable)?;
let last = pcurve.control_points.len() - 1;
for (index, uv) in [(0, old_start_uv), (last, old_end_uv)] {
let point = &mut pcurve.control_points[index];
point.x = uv[0] * point.w;
point.y = uv[1] * point.w;
}
Ok(pcurve)
}
#[allow(clippy::too_many_arguments)]
fn accept_refit(
surface: &crate::NurbsSurface,
lp: &crate::topology::LoopRecord,
coedge: &crate::topology::CoedgeRecord,
coedge_index: usize,
edge: &crate::topology::EdgeRecord,
pcurve: crate::NurbsCurve,
bar: f64,
(old_start, old_end): ((Vec3, Vec3), (Vec3, Vec3)),
entry: &mut TrimRefitEntry,
) -> Result<crate::NurbsCurve, TrimRefitDecline> {
let stations = reread_stations(&coedge.pcurve, &pcurve, edge, coedge.forward).ok_or(TrimRefitDecline::Unreadable)?;
let reread = worst_same_parameter(surface, &pcurve, edge, coedge.forward, &stations).ok_or(TrimRefitDecline::Unreadable)?;
entry.reread = Some(reread);
if reread > bar {
return Err(TrimRefitDecline::RereadOverBar);
}
let new_start = coedge_points(surface, &pcurve, edge, coedge.forward, 0.0).ok_or(TrimRefitDecline::Unreadable)?;
let new_end = coedge_points(surface, &pcurve, edge, coedge.forward, 1.0).ok_or(TrimRefitDecline::Unreadable)?;
let start_moved = new_start.0.sub(old_start.0).length();
let end_moved = new_end.0.sub(old_end.0).length();
if !(start_moved.is_finite() && end_moved.is_finite()) {
return Err(TrimRefitDecline::Unreadable);
}
if start_moved > bar || end_moved > bar {
return Err(TrimRefitDecline::EndMoved);
}
let count = lp.coedges.len();
let joints: Vec<(&crate::NurbsCurve, f64, f64)> = if count == 1 {
vec![(&coedge.pcurve, 1.0, 0.0)]
} else {
let previous = &lp.coedges[(coedge_index + count - 1) % count];
let next = &lp.coedges[(coedge_index + 1) % count];
vec![(&previous.pcurve, 1.0, 0.0), (&next.pcurve, 0.0, 1.0)]
};
let gap = |a: [f64; 2], b: [f64; 2]| ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt();
for (theirs, their_fraction, own_fraction) in joints {
let theirs_new: &crate::NurbsCurve = if count == 1 { &pcurve } else { theirs };
let theirs_old_uv = uv_at(theirs, their_fraction).ok_or(TrimRefitDecline::Unreadable)?;
let theirs_new_uv = uv_at(theirs_new, their_fraction).ok_or(TrimRefitDecline::Unreadable)?;
let old_uv = uv_at(&coedge.pcurve, own_fraction).ok_or(TrimRefitDecline::Unreadable)?;
let new_uv = uv_at(&pcurve, own_fraction).ok_or(TrimRefitDecline::Unreadable)?;
let (before, after) = (gap(theirs_old_uv, old_uv), gap(theirs_new_uv, new_uv));
if !(before.is_finite() && after.is_finite()) {
return Err(TrimRefitDecline::Unreadable);
}
if after > before {
return Err(TrimRefitDecline::UvJointWorsened);
}
let point = |uv: [f64; 2]| surface.evaluate_extended(uv[0], uv[1]).map_err(|_| TrimRefitDecline::Unreadable);
let (before, after) = (
point(theirs_old_uv)?.sub(point(old_uv)?).length(),
point(theirs_new_uv)?.sub(point(new_uv)?).length(),
);
if !(before.is_finite() && after.is_finite()) {
return Err(TrimRefitDecline::Unreadable);
}
if after > before {
return Err(TrimRefitDecline::JointWorsened);
}
}
Ok(pcurve)
}
const TRIM_REFIT_SPAN_BUDGET: usize = 2 * crate::soundness::VECTOR_AREA_SPAN_BUDGET;
fn trim_refit_gate(before: &BrepSolid, after: &BrepSolid, policy: &KernelTolerances) -> Option<TrimRefitRollback> {
trim_refit_gate_with_budget(before, after, policy, TRIM_REFIT_SPAN_BUDGET)
}
fn trim_refit_gate_with_budget(
before: &BrepSolid,
after: &BrepSolid,
policy: &KernelTolerances,
span_budget: usize,
) -> Option<TrimRefitRollback> {
if !issues_sub_multiset(&after.validate_with_tolerances(policy), &before.validate_with_tolerances(policy)) {
return Some(TrimRefitRollback::NewOrChangedIssue);
}
let summary = |report: crate::VectorAreaReport| -> (Vec<(u64, crate::ClosureReading)>, Vec<String>) {
(report.shells.iter().map(|shell| (shell.shell, shell.reading())).collect(), report.unreadable)
};
let (shells_before, unreadable_before) = summary(crate::soundness::shell_vector_areas_with_budget(before, span_budget));
let (shells_after, unreadable_after) = summary(crate::soundness::shell_vector_areas_with_budget(after, span_budget));
let verdict = vector_areas_not_worse((&shells_before, &unreadable_before), (&shells_after, &unreadable_after));
if trim_refit_tracing().is_some() {
for (label, shells, unreadable) in [("before", &shells_before, &unreadable_before), ("after", &shells_after, &unreadable_after)] {
for (shell, reading) in shells.iter() {
eprintln!("TRIM-REFIT-GATE {label} shell {shell} {reading:?}");
}
for row in unreadable.iter() {
eprintln!("TRIM-REFIT-GATE {label} unreadable {row}");
}
}
eprintln!("TRIM-REFIT-GATE verdict {verdict:?} ({})", vector_area_verdict_reason((&shells_before, &unreadable_before), (&shells_after, &unreadable_after)));
}
verdict
}
fn vector_area_verdict_reason(
(shells_before, unreadable_before): (&[(u64, crate::ClosureReading)], &[String]),
(shells_after, unreadable_after): (&[(u64, crate::ClosureReading)], &[String]),
) -> String {
for (label, shells) in [("before", shells_before), ("after", shells_after)] {
if let Some((shell, reading)) = shells.iter().find(|(_, reading)| matches!(reading, crate::ClosureReading::Unmeasured { .. })) {
return format!("shell {shell} unmeasured {label}: {reading:?}");
}
}
let mut available: HashMap<&str, usize> = HashMap::default();
for row in unreadable_before {
*available.entry(row.as_str()).or_default() += 1;
}
for row in unreadable_after {
match available.get_mut(row.as_str()) {
Some(count) if *count > 0 => *count -= 1,
_ => return format!("new unreadable row after: {row}"),
}
}
use crate::ClosureReading::{Measured, Open};
for (shell, reading) in shells_before {
let now = shells_after.iter().find(|(candidate, _)| candidate == shell).map(|(_, now)| now);
let worse = match (reading, now) {
(Measured { residual: old, .. }, Some(Measured { residual, .. })) | (Open { residual: old, .. }, Some(Open { residual, .. })) => {
!(old.is_finite() && residual.is_finite() && residual <= old)
}
_ => true,
};
if worse {
return format!("shell {shell}: before {reading:?}, after {now:?}");
}
}
if let Some((shell, reading)) = shells_after.iter().find(|(shell, _)| !shells_before.iter().any(|(before, _)| before == shell)) {
return format!("shell {shell} read after only: {reading:?}");
}
"no row".to_string()
}
fn vector_areas_not_worse(
(shells_before, unreadable_before): (&[(u64, crate::ClosureReading)], &[String]),
(shells_after, unreadable_after): (&[(u64, crate::ClosureReading)], &[String]),
) -> Option<TrimRefitRollback> {
if shells_before.iter().chain(shells_after).any(|(_, reading)| matches!(reading, crate::ClosureReading::Unmeasured { .. })) {
return Some(TrimRefitRollback::VectorAreaUnmeasured);
}
let mut available: HashMap<&str, usize> = HashMap::default();
for row in unreadable_before {
*available.entry(row.as_str()).or_default() += 1;
}
for row in unreadable_after {
match available.get_mut(row.as_str()) {
Some(count) if *count > 0 => *count -= 1,
_ => return Some(TrimRefitRollback::VectorAreaUnreadable),
}
}
use crate::ClosureReading::{Measured, Open, Unmeasured};
for (shell, reading) in shells_before {
let old = match reading {
Measured { residual, .. } | Open { residual, .. } => *residual,
Unmeasured { .. } => return Some(TrimRefitRollback::VectorAreaUnmeasured),
};
if !old.is_finite() {
return Some(TrimRefitRollback::VectorAreaUnreadable);
}
let Some((_, now)) = shells_after.iter().find(|(candidate, _)| candidate == shell) else {
return Some(TrimRefitRollback::VectorAreaUnreadable);
};
let residual = match (reading, now) {
(Measured { .. }, Measured { residual, .. }) | (Open { .. }, Open { residual, .. }) => *residual,
(_, Unmeasured { .. }) => return Some(TrimRefitRollback::VectorAreaUnmeasured),
_ => return Some(TrimRefitRollback::VectorAreaReadingChanged),
};
if !residual.is_finite() {
return Some(TrimRefitRollback::VectorAreaUnreadable);
}
if residual > old {
return Some(TrimRefitRollback::VectorAreaWorsened);
}
}
if shells_after.iter().any(|(shell, _)| !shells_before.iter().any(|(before, _)| before == shell)) {
return Some(TrimRefitRollback::VectorAreaReadingChanged);
}
None
}
fn issues_sub_multiset(after: &[crate::topology::ValidationIssue], before: &[crate::topology::ValidationIssue]) -> bool {
let key = |issue: &crate::topology::ValidationIssue| (issue.severity, format!("{:?}", issue.kind), issue.message.clone());
let mut available: HashMap<(&'static str, String, String), usize> = HashMap::default();
for issue in before {
*available.entry(key(issue)).or_default() += 1;
}
for issue in after {
match available.get_mut(&key(issue)) {
Some(count) if *count > 0 => *count -= 1,
_ => return false,
}
}
true
}
fn coedge_points(
surface: &crate::NurbsSurface,
pcurve: &crate::NurbsCurve,
edge: &crate::topology::EdgeRecord,
forward: bool,
fraction: f64,
) -> Option<(Vec3, Vec3)> {
let [q0, q1] = pcurve.domain().ok()?;
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction).ok()?;
let image = surface.evaluate_extended(uv.x, uv.y).ok()?;
let t = if forward {
edge.t0 + (edge.t1 - edge.t0) * fraction
} else {
edge.t1 - (edge.t1 - edge.t0) * fraction
};
let point = edge.curve.evaluate(t).ok()?;
let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
(uv.x.is_finite() && uv.y.is_finite() && finite(image) && finite(point)).then_some((image, point))
}
fn uv_at(pcurve: &crate::NurbsCurve, fraction: f64) -> Option<[f64; 2]> {
let [q0, q1] = pcurve.domain().ok()?;
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction).ok()?;
(uv.x.is_finite() && uv.y.is_finite()).then_some([uv.x, uv.y])
}
fn worst_same_parameter(
surface: &crate::NurbsSurface,
pcurve: &crate::NurbsCurve,
edge: &crate::topology::EdgeRecord,
forward: bool,
stations: &[f64],
) -> Option<f64> {
let mut worst = 0.0_f64;
for &fraction in stations {
let (image, point) = coedge_points(surface, pcurve, edge, forward, fraction)?;
let distance = image.sub(point).length();
if !distance.is_finite() {
return None;
}
worst = worst.max(distance);
}
Some(worst)
}
fn curve_is_finite(curve: &crate::NurbsCurve) -> bool {
curve.knots.iter().all(|knot| knot.is_finite())
&& curve.control_points.iter().all(|point| [point.x, point.y, point.z, point.w].iter().all(|value| value.is_finite()))
}
fn pcurve_breaks(pcurve: &crate::NurbsCurve) -> Option<Vec<f64>> {
if !curve_is_finite(pcurve) {
return None;
}
let [q0, q1] = pcurve.domain().ok()?;
if !(q0.is_finite() && q1.is_finite() && q1 > q0) {
return None;
}
let mut breaks = vec![0.0, 1.0];
breaks.extend(pcurve.knots.iter().filter(|&&knot| knot > q0 && knot < q1).map(|&knot| (knot - q0) / (q1 - q0)));
Some(breaks)
}
fn edge_breaks(edge: &crate::topology::EdgeRecord, forward: bool) -> Option<Vec<f64>> {
let span = edge.t1 - edge.t0;
if !(edge.t0.is_finite() && edge.t1.is_finite() && span != 0.0 && curve_is_finite(&edge.curve)) {
return None;
}
let (low, high) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
let mut breaks = vec![0.0, 1.0];
breaks.extend(edge.curve.knots.iter().filter(|&&knot| knot > low && knot < high).map(|&knot| {
if forward {
(knot - edge.t0) / span
} else {
(edge.t1 - knot) / span
}
}));
Some(breaks)
}
fn merged_breaks(sets: &[Vec<f64>]) -> Option<Vec<f64>> {
let mut all: Vec<f64> = sets.iter().flatten().copied().collect();
if !all.iter().all(|value| value.is_finite()) {
return None;
}
all.sort_by(f64::total_cmp);
all.dedup();
Some(all)
}
fn span_stations(breaks: &[f64], interior: usize) -> Vec<f64> {
let mut stations: Vec<f64> = breaks.to_vec();
for pair in breaks.windows(2) {
let (a, b) = (pair[0], pair[1]);
if !(b > a) {
continue;
}
stations.push(0.5 * (a + b));
for index in 1..=interior {
stations.push(a + (b - a) * index as f64 / (interior + 1) as f64);
}
}
stations
}
fn detection_stations(pcurve: &crate::NurbsCurve, edge: &crate::topology::EdgeRecord, forward: bool) -> Option<Vec<f64>> {
let mut stations = span_stations(&merged_breaks(&[pcurve_breaks(pcurve)?])?, TRIM_REFIT_DETECT_INTERIOR);
stations.extend(span_stations(&merged_breaks(&[edge_breaks(edge, forward)?])?, 0));
Some(stations)
}
fn reread_stations(
old: &crate::NurbsCurve,
new: &crate::NurbsCurve,
edge: &crate::topology::EdgeRecord,
forward: bool,
) -> Option<Vec<f64>> {
let breaks = merged_breaks(&[pcurve_breaks(old)?, pcurve_breaks(new)?, edge_breaks(edge, forward)?])?;
Some(span_stations(&breaks, TRIM_REFIT_REREAD_INTERIOR))
}
fn scope_stations(trims: &[&crate::NurbsCurve], edge: &crate::topology::EdgeRecord, forward: bool, interior: usize) -> Option<Vec<f64>> {
let mut sets = Vec::with_capacity(trims.len() + 1);
for trim in trims {
sets.push(pcurve_breaks(trim)?);
}
sets.push(edge_breaks(edge, forward)?);
Some(span_stations(&merged_breaks(&sets)?, interior))
}
#[derive(Clone, Copy, Debug)]
struct ScopeFoot {
distance: f64,
uv: [f64; 2],
beyond_u: f64,
beyond_v: f64,
}
#[derive(Clone, Copy, Debug)]
struct ScopeRead {
worst: f64,
outside: usize,
witnessed: usize,
extension: Option<TrimRefitDecline>,
}
fn extension_kind(surface: &crate::NurbsSurface, foot: &ScopeFoot) -> Option<TrimRefitDecline> {
let (past_u, past_v) = (foot.beyond_u > 0.0, foot.beyond_v > 0.0);
if past_u && past_v {
return Some(TrimRefitDecline::CornerExtension);
}
for (past, degree, beyond, along_u) in [
(past_u, surface.degree_u, foot.beyond_u, true),
(past_v, surface.degree_v, foot.beyond_v, false),
] {
if !past {
continue;
}
if degree > 1 {
return Some(TrimRefitDecline::CurvedExtension);
}
match native_reach(surface, along_u, foot.uv) {
Some(reach) if beyond < reach => {}
Some(_) => return Some(TrimRefitDecline::BeyondNativeReach),
None => return Some(TrimRefitDecline::Unreadable),
}
}
None
}
fn hint_only_distance(surface: &crate::NurbsSurface, point: Vec3, seed: [f64; 2]) -> Option<f64> {
const ITERATIONS: usize = 50;
let distance = |u: f64, v: f64| -> Option<f64> {
let d = surface.evaluate_extended(u, v).ok()?.sub(point).length();
d.is_finite().then_some(d)
};
let [mut u, mut v] = seed;
let mut best = distance(u, v)?;
for _ in 0..ITERATIONS {
let Ok(derivatives) = surface.derivatives_extended(u, v, 1) else { break };
let residual = derivatives[0][0].sub(point);
let (su, sv) = (derivatives[1][0], derivatives[0][1]);
let (a, b, c) = (su.dot(su), su.dot(sv), sv.dot(sv));
let (gu, gv) = (su.dot(residual), sv.dot(residual));
let determinant = a * c - b * b;
if !(determinant.is_finite() && determinant.abs() > 0.0) {
break;
}
let du = -(c * gu - b * gv) / determinant;
let dv = -(a * gv - b * gu) / determinant;
if !(du.is_finite() && dv.is_finite()) {
break;
}
u += du;
v += dv;
if let Some(d) = distance(u, v) {
best = best.min(d);
}
if du.abs() <= 1e-15 * (1.0 + u.abs()) && dv.abs() <= 1e-15 * (1.0 + v.abs()) {
break;
}
}
Some(best)
}
fn carrier_witness(surface: &crate::NurbsSurface, foot: &ScopeFoot, point: Vec3) -> Option<f64> {
let [u0, u1] = surface.domain_u().ok()?;
let [v0, v1] = surface.domain_v().ok()?;
let on = surface.evaluate(foot.uv[0].clamp(u0, u1), foot.uv[1].clamp(v0, v1)).ok()?;
let distance = on.sub(point).length();
distance.is_finite().then_some(distance)
}
fn native_reach(surface: &crate::NurbsSurface, along_u: bool, uv: [f64; 2]) -> Option<f64> {
let [u0, u1] = surface.domain_u().ok()?;
let [v0, v1] = surface.domain_v().ok()?;
let (knots, low, high, at, other) = if along_u {
(&surface.knots_u, u0, u1, uv[0], uv[1].clamp(v0, v1))
} else {
(&surface.knots_v, v0, v1, uv[1], uv[0].clamp(u0, u1))
};
if !(at.is_finite() && other.is_finite()) {
return None;
}
let (boundary, inner) = if at > high {
(high, knots.iter().copied().filter(|&knot| knot < high).fold(f64::NEG_INFINITY, f64::max))
} else if at < low {
(low, knots.iter().copied().filter(|&knot| knot > low).fold(f64::INFINITY, f64::min))
} else {
return Some(f64::INFINITY);
};
if !inner.is_finite() {
return None;
}
let point = |t: f64| if along_u { surface.evaluate(t, other) } else { surface.evaluate(other, t) };
let (pb, pa, pm) = (point(boundary).ok()?, point(inner).ok()?, point(0.5 * (boundary + inner)).ok()?);
let (to_mid, from_mid, length) = (pm.sub(pa).length(), pb.sub(pm).length(), pb.sub(pa).length());
if !(to_mid.is_finite() && from_mid.is_finite() && length.is_finite() && from_mid > 0.0) {
return None;
}
let ratio = to_mid / from_mid;
Some(if ratio <= 1.0 { f64::INFINITY } else { length / (ratio - 1.0) })
}
#[cfg_attr(not(test), allow(dead_code))]
fn edge_standoff(
surface: &crate::NurbsSurface,
edge: &crate::topology::EdgeRecord,
forward: bool,
stations: Option<&[f64]>,
model: f64,
) -> Option<ScopeRead> {
edge_standoff_replay(surface, edge, forward, stations, model, None, None)
}
type StationRecord = Vec<(u64, [f64; 2])>;
type ClaimRecord = Vec<((usize, usize), StationRecord)>;
type StationReplay = rustc_hash::FxHashMap<u64, (Option<[f64; 2]>, [f64; 2])>;
fn station_replay(record: &StationRecord) -> StationReplay {
let mut replay = StationReplay::default();
for (index, &(bits, uv)) in record.iter().enumerate() {
let incoming = index.checked_sub(1).map(|previous| record[previous].1);
replay.insert(bits, (incoming, uv));
}
replay
}
fn same_hint(a: Option<[f64; 2]>, b: Option<[f64; 2]>) -> bool {
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => a[0].to_bits() == b[0].to_bits() && a[1].to_bits() == b[1].to_bits(),
_ => false,
}
}
fn edge_standoff_replay(
surface: &crate::NurbsSurface,
edge: &crate::topology::EdgeRecord,
forward: bool,
stations: Option<&[f64]>,
model: f64,
replay: Option<&StationReplay>,
record: Option<&mut StationRecord>,
) -> Option<ScopeRead> {
edge_standoff_read(surface, edge, forward, stations, model, replay, record, None)
}
#[allow(clippy::too_many_arguments)]
fn edge_standoff_read(
surface: &crate::NurbsSurface,
edge: &crate::topology::EdgeRecord,
forward: bool,
stations: Option<&[f64]>,
model: f64,
replay: Option<&StationReplay>,
mut record: Option<&mut StationRecord>,
candidacy_miss: Option<f64>,
) -> Option<ScopeRead> {
let stations = stations?;
if stations.is_empty() {
return None;
}
let mut read = ScopeRead { worst: 0.0, outside: 0, witnessed: 0, extension: None };
let mut hint: Option<[f64; 2]> = None;
for &fraction in stations {
let t = if forward {
edge.t0 + (edge.t1 - edge.t0) * fraction
} else {
edge.t1 - (edge.t1 - edge.t0) * fraction
};
let point = edge.curve.evaluate(t).ok()?;
if !(point.x.is_finite() && point.y.is_finite() && point.z.is_finite()) {
return None;
}
profiling(|profile| {
match profile.phase {
ScopePhase::Candidacy => profile.candidacy_feet += 1,
ScopePhase::OldClaim => profile.old_claim_feet += 1,
ScopePhase::FinalClaim => profile.final_claim_feet += 1,
}
if !profile.keys.insert((surface as *const crate::NurbsSurface as usize, edge.id, t.to_bits())) {
profile.repeated_feet[profile.phase as usize] += 1;
}
});
let replayed = replay
.and_then(|replay| replay.get(&fraction.to_bits()))
.filter(|(incoming, _)| same_hint(*incoming, hint))
.map(|&(_, uv)| uv);
let foot = match replayed {
Some(uv) => {
profiling(|profile| profile.replayed_feet += 1);
foot_at_uv(surface, point, uv)?
}
None => unclamped_foot(surface, point, hint)?,
};
if let Some(record) = record.as_mut() {
record.push((fraction.to_bits(), foot.uv));
}
hint = Some(foot.uv);
if foot.beyond_u > 0.0 || foot.beyond_v > 0.0 {
read.outside += 1;
}
let distance = match extension_kind(surface, &foot) {
None => foot.distance,
Some(kind) => {
let witness = carrier_witness(surface, &foot, point)?;
profiling(|profile| profile.witness_evaluations += 1);
read.witnessed += 1;
if witness > model && read.extension.is_none() {
read.extension = Some(kind);
}
witness
}
};
read.worst = read.worst.max(distance);
if candidacy_miss.is_some_and(|miss| miss <= crate::PCURVE_REFINEMENT_TOLERANCE + read.worst) {
break;
}
}
Some(read)
}
fn unclamped_foot(surface: &crate::NurbsSurface, point: Vec3, hint: Option<[f64; 2]>) -> Option<ScopeFoot> {
const GRID: usize = 8;
const SEEDS: usize = 3;
const ITERATIONS: usize = 50;
let [u0, u1] = surface.domain_u().ok()?;
let [v0, v1] = surface.domain_v().ok()?;
surface.closed_directions().ok()?;
let distance = |u: f64, v: f64| -> Option<f64> {
let d = surface.evaluate_extended(u, v).ok()?.sub(point).length();
d.is_finite().then_some(d)
};
let mut seeds: Vec<[f64; 2]> = Vec::new();
let projector_started = profiling_on().then(web_time::Instant::now);
let projected = crate::project_point_to_surface(surface, point);
if let Some(started) = projector_started {
profiling(|profile| {
profile.projector_calls += 1;
profile.projector_seconds += started.elapsed().as_secs_f64();
});
}
if let Ok(foot) = projected {
seeds.push([foot.u, foot.v]);
}
seeds.extend(hint);
let mut scored: Vec<(f64, [f64; 2])> = seeds
.into_iter()
.filter_map(|seed| distance(seed[0], seed[1]).map(|d| (d, seed)))
.collect();
if scored.is_empty() {
profiling(|profile| profile.grid_fallbacks += 1);
for i in 0..=GRID {
for j in 0..=GRID {
let seed = [u0 + (u1 - u0) * i as f64 / GRID as f64, v0 + (v1 - v0) * j as f64 / GRID as f64];
if let Some(d) = distance(seed[0], seed[1]) {
scored.push((d, seed));
}
}
}
}
if scored.is_empty() {
return None;
}
scored.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut best = (f64::INFINITY, [f64::NAN; 2]);
let mut iterations = 0_usize;
for &(start, [mut u, mut v]) in scored.iter().take(SEEDS) {
if start < best.0 {
best = (start, [u, v]);
}
for _ in 0..ITERATIONS {
iterations += 1;
let Ok(derivatives) = surface.derivatives_extended(u, v, 1) else { break };
let residual = derivatives[0][0].sub(point);
let (su, sv) = (derivatives[1][0], derivatives[0][1]);
let (a, b, c) = (su.dot(su), su.dot(sv), sv.dot(sv));
let (gu, gv) = (su.dot(residual), sv.dot(residual));
let determinant = a * c - b * b;
if !(determinant.is_finite() && determinant.abs() > 0.0) {
break;
}
let du = -(c * gu - b * gv) / determinant;
let dv = -(a * gv - b * gu) / determinant;
if !(du.is_finite() && dv.is_finite()) {
break;
}
u += du;
v += dv;
if let Some(d) = distance(u, v) {
if d < best.0 {
best = (d, [u, v]);
}
}
if du.abs() <= 1e-15 * (1.0 + u.abs()) && dv.abs() <= 1e-15 * (1.0 + v.abs()) {
break;
}
}
}
profiling(|profile| profile.newton_iterations += iterations);
if let Some(seed) = hint.filter(|_| profiling_on()) {
let sampled = SCOPE_PROFILE.with(|cell| {
cell.borrow_mut().as_mut().is_some_and(|profile| {
profile.sample_clock += 1;
profile.sample_clock % SCOPE_PROFILE_SAMPLE == 0
})
});
if sampled {
let alone = hint_only_distance(surface, point, seed);
profiling(|profile| {
profile.sampled += 1;
match alone {
None => profile.hint_unreadable += 1,
Some(value) if (value - best.0).abs() <= 1e-12 => profile.hint_agrees += 1,
Some(value) if value < best.0 => profile.hint_smaller += 1,
Some(_) => profile.hint_larger += 1,
}
});
}
}
let (d, [u, v]) = best;
finish_foot(surface, d, u, v)
}
fn foot_at_uv(surface: &crate::NurbsSurface, point: Vec3, [u, v]: [f64; 2]) -> Option<ScopeFoot> {
let d = surface.evaluate_extended(u, v).ok()?.sub(point).length();
if !d.is_finite() {
return None;
}
finish_foot(surface, d, u, v)
}
fn finish_foot(surface: &crate::NurbsSurface, d: f64, u: f64, v: f64) -> Option<ScopeFoot> {
let [u0, u1] = surface.domain_u().ok()?;
let [v0, v1] = surface.domain_v().ok()?;
let (closed_u, closed_v) = surface.closed_directions().ok()?;
if !(d.is_finite() && u.is_finite() && v.is_finite()) {
return None;
}
let out = |x: f64, low: f64, high: f64, closed: bool| -> f64 {
if closed {
0.0
} else if x < low {
low - x
} else if x > high {
x - high
} else {
0.0
}
};
let (du_out, dv_out) = (out(u, u0, u1, closed_u), out(v, v0, v1, closed_v));
let (beyond_u, beyond_v) = if du_out > 0.0 || dv_out > 0.0 {
let derivatives = surface.derivatives_extended(u, v, 1).ok()?;
let (speed_u, speed_v) = (derivatives[1][0].length(), derivatives[0][1].length());
if !(speed_u.is_finite() && speed_v.is_finite()) {
return None;
}
(du_out * speed_u, dv_out * speed_v)
} else {
(0.0, 0.0)
};
Some(ScopeFoot { distance: d, uv: [u, v], beyond_u, beyond_v })
}