mod carve;
mod chain;
mod collapse;
mod corner;
mod edge;
mod fold;
mod miter;
mod network;
mod planar_chart;
mod restrict;
pub(crate) mod runout;
mod stations;
mod rows;
mod track_fit;
thread_local! {
static REFINE_OFF_FOR_TEST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn station_refinement_on() -> bool {
std::env::var("BREP_BLEND_REFINE").as_deref() != Ok("0")
&& !REFINE_OFF_FOR_TEST.with(|off| off.get())
}
thread_local! {
static BLEND_NOTES: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
}
pub(crate) fn record_blend_note(note: String) {
BLEND_NOTES.with(|notes| notes.borrow_mut().push(note));
}
pub fn take_blend_notes() -> Vec<String> {
BLEND_NOTES.with(|notes| std::mem::take(&mut *notes.borrow_mut()))
}
pub(crate) fn blend_notes_mark() -> usize {
BLEND_NOTES.with(|notes| notes.borrow().len())
}
pub(crate) fn blend_notes_truncate(mark: usize) {
BLEND_NOTES.with(|notes| notes.borrow_mut().truncate(mark));
}
#[derive(Clone, Debug)]
pub(crate) struct WallModelReport {
pub(crate) name: Option<String>,
pub(crate) surface: crate::NurbsSurface,
pub(crate) request: f64,
pub(crate) residual: f64,
pub(crate) detail: Option<String>,
pub(crate) budget: crate::ApproximationBudget,
}
thread_local! {
static BLEND_APPROXIMATIONS: std::cell::RefCell<Vec<WallModelReport>> = const { std::cell::RefCell::new(Vec::new()) };
}
thread_local! {
static BLEND_OPERATION_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(crate) struct BlendOperation;
impl BlendOperation {
pub(crate) fn enter() -> BlendOperation {
BLEND_OPERATION_DEPTH.with(|depth| {
if depth.get() == 0 {
BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().clear());
}
depth.set(depth.get() + 1);
});
BlendOperation
}
}
impl Drop for BlendOperation {
fn drop(&mut self) {
BLEND_OPERATION_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
pub(crate) fn record_wall_model_report(report: WallModelReport) {
BLEND_APPROXIMATIONS.with(|reports| reports.borrow_mut().push(report));
}
pub(crate) fn blend_approximations_pending() -> bool {
BLEND_APPROXIMATIONS.with(|reports| !reports.borrow().is_empty())
}
pub(crate) fn same_surface(a: &crate::NurbsSurface, b: &crate::NurbsSurface) -> bool {
let bits = |values: &[f64]| values.iter().map(|value| value.to_bits()).collect::<Vec<_>>();
a.degree_u == b.degree_u
&& a.degree_v == b.degree_v
&& bits(&a.knots_u) == bits(&b.knots_u)
&& bits(&a.knots_v) == bits(&b.knots_v)
&& a.control_points.len() == b.control_points.len()
&& a.control_points.iter().zip(&b.control_points).all(|(row_a, row_b)| {
row_a.len() == row_b.len()
&& row_a.iter().zip(row_b).all(|(p, q)| {
[p.x, p.y, p.z, p.w].map(f64::to_bits) == [q.x, q.y, q.z, q.w].map(f64::to_bits)
})
})
}
fn names_the_wall(face_name: &Option<String>, report_name: &Option<String>) -> bool {
match (face_name, report_name) {
(Some(face), Some(report)) => {
face == report
|| face.strip_prefix(report.as_str()).and_then(|rest| rest.strip_prefix('[')).and_then(|rest| rest.strip_suffix(']')).is_some_and(
|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()),
)
}
(None, None) => true,
_ => false,
}
}
pub fn take_blend_approximations(bodies: &[(&str, &crate::BrepSolid)]) -> Vec<crate::Approximation> {
let snapshot = BLEND_APPROXIMATIONS.with(|reports| std::mem::take(&mut *reports.borrow_mut()));
let mut unique: Vec<WallModelReport> = Vec::new();
for report in snapshot {
unique.retain(|kept| !(kept.name == report.name && same_surface(&kept.surface, &report.surface)));
unique.push(report);
}
let mut approximations = Vec::new();
for (body, solid) in bodies {
for report in &unique {
let carried = solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
names_the_wall(&face.name, &report.name) && same_surface(&face.surface, &report.surface)
});
if carried {
let wall = report.name.as_deref().unwrap_or("(unnamed)");
let unaccepted = matches!(report.budget.reason, crate::BudgetReason::Unaccepted | crate::BudgetReason::Unread);
let budget = match report.budget.mechanism {
crate::BudgetMechanism::LocalRounds => format!(
"local refinement stopped: {:?} after {} of {} rounds, {} of {} stations per segment",
report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
report.budget.stations, report.budget.station_limit,
),
crate::BudgetMechanism::StationRungs => format!(
"the open march's station ladder stopped: {:?} after {} of {} global station rungs, {} of {} stations",
report.budget.reason, report.budget.rounds_used, report.budget.rounds_limit,
report.budget.stations, report.budget.station_limit,
),
};
let unknown = report.budget.unread > 0;
let rails = report.budget.measured_component == crate::MeasuredComponent::Rails;
let mut message = if !unaccepted && (rails || unknown) {
let what = if rails {
format!("the fillet wall {wall}'s support rails stand")
} else {
format!("the fillet wall {wall} stands")
};
let against = if rails {
"off their exact construction (their carriers / the exact ball contacts)"
} else {
"from the rolling ball between its rails"
};
let reading = if unknown {
format!(
"an UNKNOWN distance {against}: {} interval(s) could not be read (the largest READABLE reading is {:.3e} mm, not the worst)",
report.budget.unread, report.residual
)
} else {
format!("{:.3e} mm {against}", report.residual)
};
format!(
"{what} {reading}, against a construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
report.request,
)
} else if unaccepted {
format!(
"the blend wall {wall} ships from a rung that never met acceptance: its worst reading \
{:.3e} mm against its acceptance bar {:.3e} mm; {budget}",
report.residual, report.request,
)
} else {
format!(
"the fillet wall {wall} stands {:.3e} mm from the rolling ball between its rails, against a \
construction request of {:.3e} mm (accepted at the intersection-fit bar); {budget}",
report.residual, report.request,
)
};
if let Some(detail) = &report.detail {
message = format!("{message} ({detail})");
}
approximations.push(crate::Approximation {
code: if unaccepted { "blend.wall_acceptance" } else { "blend.wall_model" }.to_string(),
body: body.to_string(),
measured: report.residual,
bar: report.request,
volume_bound: None,
edges: Vec::new(),
budget: Some(report.budget.clone()),
message,
});
}
}
}
approximations
}
pub use chain::{blend_smooth_chain, blend_smooth_chain_if_closed};
pub(crate) use corner::round_concave_chain_corner;
pub use corner::round_convex_corner;
pub use edge::{blend_closed_edge, blend_edge_variable, blend_open_edge};
pub(crate) use collapse::is_full_width_collapse;
pub(crate) use planar_chart::{
fit_planar_charts_to_trims, is_planar_chart_refusal, planar_chart_overrun,
};
pub(crate) use track_fit::PCURVE_OFF_FLOOR;
pub(crate) use edge::{consumed_band, is_consumed_snap, is_rail_collapse};
pub(crate) use network::{
blend_star_network, blend_star_network_with_corner_policy, degenerate_corner_setbacks, mixed_convexity_corner, mixed_convexity_corners, selection_convexity, DegenerateSetback,
MixedCorner,
};
pub(crate) use miter::{is_marched_fit_off_carriers, is_miter_support_overrun};
pub(crate) use runout::{build_runout_walls, plan_runout, take_runout_reports, RunoutPlan, RunoutWallReport};
pub(crate) use fold::{is_wall_fold, WALL_FOLDS};
pub(crate) use stations::is_ball_off_carrier;