mod carve;
mod chain;
mod collapse;
pub(crate) mod convex_parallel;
mod corner;
pub(crate) mod creased_rim;
mod edge;
mod fold;
mod miter;
mod network;
pub(crate) use network::STRIPE_PINCH_TAPER_WHAT;
mod planar_chart;
mod restrict;
pub(super) mod rollover;
mod rollover_surgery;
pub(crate) mod runout;
pub(crate) mod split_rim;
pub(crate) mod sheet_ends;
mod stations;
mod rows;
mod track_fit;
pub(crate) const TANGENTIAL_TRANSVERSE_END: &str = "tangential_transverse_end";
pub(crate) fn is_tangential_transverse_end(refusal: &crate::KernelRefusal) -> bool {
matches!(&refusal.class, crate::RefusalClass::UnsupportedGeometry { what }
if what == TANGENTIAL_TRANSVERSE_END)
}
pub(crate) fn station_refinement_on() -> bool {
switch!(live "BREP_BLEND_REFINE", true)
}
thread_local! {
static BLEND_NOTES: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
}
thread_local! {
static OPEN_PAD_NETWORK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn with_open_pad_network<T>(build: impl FnOnce() -> T) -> T {
struct Reset(bool);
impl Drop for Reset {
fn drop(&mut self) {
OPEN_PAD_NETWORK.with(|flag| flag.set(self.0));
}
}
let _reset = Reset(OPEN_PAD_NETWORK.with(|flag| flag.replace(true)));
build()
}
pub(in crate::blend) fn open_pad_network_on() -> bool {
OPEN_PAD_NETWORK.with(|flag| flag.get())
}
const CROSSED_RAILS_DEFAULT: bool = true;
pub(crate) fn crossed_rails_on() -> bool {
switch!(live "BREP_BLEND_CROSSED_RAILS", CROSSED_RAILS_DEFAULT)
}
const NO_COMMON_BALL_STAR_DEFAULT: bool = true;
pub(crate) fn no_common_ball_star_on() -> bool {
switch!(live "BREP_BLEND_RIBBON_STAR", NO_COMMON_BALL_STAR_DEFAULT)
}
const RIDGE_STAR_DEFAULT: bool = true;
pub(crate) fn ridge_star_on() -> bool {
switch!(live "BREP_BLEND_RIDGE_STAR", RIDGE_STAR_DEFAULT)
}
const UNEQUAL_MITER_DEFAULT: bool = true;
pub(crate) fn unequal_miter_on() -> bool {
switch!(live "BREP_BLEND_UNEQUAL_MITER", UNEQUAL_MITER_DEFAULT)
}
const ROLLOVER_DEFAULT: bool = true;
pub(crate) fn rollover_on() -> bool {
switch!(live "BREP_BLEND_ROLLOVER", ROLLOVER_DEFAULT)
}
const STOPPED_END_ROLLOVER_DEFAULT: bool = false;
pub(crate) fn stopped_end_rollover_on() -> bool {
switch!(live "BREP_BLEND_STOPPED_END_ROLLOVER", STOPPED_END_ROLLOVER_DEFAULT)
}
const EXACT_LANE_DEFAULT: bool = true;
const EXACT_RIM_LANE_DEFAULT: bool = true;
pub(crate) fn exact_rim_lane_on() -> bool {
switch!(live "BREP_BLEND_EXACT_RIM", EXACT_RIM_LANE_DEFAULT)
}
pub(crate) fn exact_lane_on() -> bool {
switch!(live "BREP_BLEND_EXACT_LANE", EXACT_LANE_DEFAULT)
}
const EXACT_REVOLUTION_DEFAULT: bool = true;
pub(crate) fn exact_revolution_on() -> bool {
switch!(live "BREP_BLEND_EXACT_REVOLUTION", EXACT_REVOLUTION_DEFAULT)
}
const EXACT_TRIM_DEFAULT: bool = true;
pub(crate) fn exact_trim_on() -> bool {
switch!(live "BREP_BLEND_EXACT_TRIM", EXACT_TRIM_DEFAULT)
}
const ACCEPT_ONCE_DEFAULT: bool = true;
pub(crate) fn accept_once_on() -> bool {
switch!(live "BREP_BLEND_ACCEPT_ONCE", ACCEPT_ONCE_DEFAULT)
}
const CHAIN_ARC_STATIONS_DEFAULT: bool = false;
pub(crate) fn chain_arc_stations_on() -> bool {
switch!(live "BREP_BLEND_CHAIN_ARC_STATIONS", CHAIN_ARC_STATIONS_DEFAULT)
}
const TAPER_NETWORK_DEFAULT: bool = true;
pub(crate) fn taper_network_on() -> bool {
switch!(live "BREP_BLEND_TAPER_NETWORK", TAPER_NETWORK_DEFAULT)
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum StripeRadius {
Constant(f64),
Stops(Vec<(f64, f64)>),
}
impl StripeRadius {
pub(crate) fn from_stops(stops: &[(f64, f64)]) -> Self {
let mut sorted = stops.to_vec();
sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
match sorted.first() {
Some(&(_, first)) if sorted.iter().all(|(_, r)| (r - first).abs() <= 1e-12 * (1.0 + first.abs())) => {
StripeRadius::Constant(first)
}
_ => StripeRadius::Stops(sorted),
}
}
pub(crate) fn at_fraction(&self, fraction: f64) -> f64 {
match self {
StripeRadius::Constant(radius) => *radius,
StripeRadius::Stops(stops) => {
let fraction = fraction.clamp(0.0, 1.0);
if fraction <= stops[0].0 {
return stops[0].1;
}
for pair in stops.windows(2) {
if fraction <= pair[1].0 {
let width = (pair[1].0 - pair[0].0).max(1e-12);
return pair[0].1 + (pair[1].1 - pair[0].1) * (fraction - pair[0].0) / width;
}
}
stops[stops.len() - 1].1
}
}
}
pub(crate) fn constant(&self) -> Option<f64> {
match self {
StripeRadius::Constant(radius) => Some(*radius),
StripeRadius::Stops(_) => None,
}
}
pub(crate) fn largest(&self) -> f64 {
match self {
StripeRadius::Constant(radius) => *radius,
StripeRadius::Stops(stops) => stops.iter().map(|(_, r)| *r).fold(0.0, f64::max),
}
}
pub(crate) fn end(&self, at_start: bool) -> f64 {
self.at_fraction(if at_start { 0.0 } else { 1.0 })
}
pub(crate) fn is_positive(&self) -> bool {
match self {
StripeRadius::Constant(radius) => radius.is_finite() && *radius > 0.0,
StripeRadius::Stops(stops) => !stops.is_empty() && stops.iter().all(|(_, r)| r.is_finite() && *r > 0.0),
}
}
}
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());
timing::begin_operation();
}
depth.set(depth.get() + 1);
});
BlendOperation
}
}
pub(crate) fn blend_operation_outermost() -> bool {
BLEND_OPERATION_DEPTH.with(|depth| depth.get() <= 1)
}
impl Drop for BlendOperation {
fn drop(&mut self) {
BLEND_OPERATION_DEPTH.with(|depth| {
depth.set(depth.get().saturating_sub(1));
if depth.get() == 0 {
timing::end_operation();
}
});
}
}
pub(crate) mod timing {
use std::cell::{Cell, RefCell};
use web_time::Instant;
#[derive(Clone, Debug, PartialEq)]
pub struct BlendTiming {
pub measure: String,
pub ms: f64,
pub count: u64,
}
thread_local! {
static CAPTURE: Cell<bool> = const { Cell::new(false) };
static ENABLED: Cell<bool> = const { Cell::new(false) };
static TO_STDERR: Cell<bool> = const { Cell::new(false) };
static LEDGER: RefCell<Vec<BlendTiming>> = const { RefCell::new(Vec::new()) };
}
pub fn blend_timing_capture(on: bool) {
CAPTURE.with(|capture| capture.set(on));
}
pub fn take_blend_timings() -> Vec<BlendTiming> {
let mut entries = LEDGER.with(|ledger| std::mem::take(&mut *ledger.borrow_mut()));
entries.sort_by(|a, b| a.measure.cmp(&b.measure));
entries
}
pub(crate) fn enabled() -> bool {
ENABLED.with(|enabled| enabled.get())
}
pub(super) fn begin_operation() {
let profile = crate::profile::profile_enabled();
TO_STDERR.with(|to_stderr| to_stderr.set(profile));
let on = profile || CAPTURE.with(|capture| capture.get());
ENABLED.with(|enabled| enabled.set(on));
if on {
LEDGER.with(|ledger| ledger.borrow_mut().clear());
}
}
pub(super) fn end_operation() {
if TO_STDERR.with(|to_stderr| to_stderr.get()) {
let mut entries = LEDGER.with(|ledger| ledger.borrow().clone());
entries.sort_by(|a, b| a.measure.cmp(&b.measure));
for entry in entries {
if entry.measure.starts_with("timing.") {
eprintln!("blend.{}={:.3} n={}", entry.measure, entry.ms, entry.count);
} else {
eprintln!("blend.{}={}", entry.measure, entry.count);
}
}
}
ENABLED.with(|enabled| enabled.set(false));
}
fn add(measure: &str, ms: f64, count: u64) {
LEDGER.with(|ledger| {
let mut ledger = ledger.borrow_mut();
match ledger.iter_mut().find(|entry| entry.measure == measure) {
Some(entry) => {
entry.ms += ms;
entry.count += count;
}
None => ledger.push(BlendTiming { measure: measure.to_string(), ms, count }),
}
});
}
pub(crate) fn record_ms(measure: &str, ms: f64) {
if enabled() {
add(measure, ms, 1);
}
}
pub(crate) fn record_count(measure: &str, n: u64) {
if enabled() {
add(measure, 0.0, n);
}
}
pub(crate) struct BlendTimer {
measure: &'static str,
started: Option<Instant>,
}
impl BlendTimer {
pub(crate) fn start(measure: &'static str) -> BlendTimer {
BlendTimer { measure, started: enabled().then(Instant::now) }
}
pub(crate) fn elapsed_ms(&self) -> f64 {
self.started.map_or(0.0, |started| started.elapsed().as_secs_f64() * 1_000.0)
}
pub(crate) fn stop(mut self) -> f64 {
let ms = self.elapsed_ms();
if self.started.take().is_some() {
add(self.measure, ms, 1);
}
ms
}
}
impl Drop for BlendTimer {
fn drop(&mut self) {
if let Some(started) = self.started.take() {
add(self.measure, started.elapsed().as_secs_f64() * 1_000.0, 1);
}
}
}
}
pub use timing::{blend_timing_capture, take_blend_timings, BlendTiming};
pub(crate) use timing::{record_count, BlendTimer};
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_closed_edge_knot_aware, blend_edge_variable, blend_open_edge, is_pcurve_floor_refusal, with_pcurve_knot_retry};
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_laws, 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) fn selection_mates<'a>(
solid: &'a crate::topology::BrepSolid,
edge_ids: &[u64],
radius: f64,
) -> Result<Vec<(&'a crate::topology::EdgeRecord, [&'a crate::topology::FaceRecord; 2])>, crate::KernelRefusal> {
Ok(network::stripe_mates(solid, edge_ids, radius)?
.into_iter()
.map(|(edge, first, second)| (edge, [first.face, second.face]))
.collect())
}
pub(crate) use runout::{build_runout_walls, plan_runout, take_runout_reports, RunoutPlan, RunoutWallReport};
pub(crate) use fold::{is_rail_fold, is_wall_fold, wall_loop_refusal, WALL_FOLDS};
pub(crate) use stations::is_ball_off_carrier;
pub(crate) use network::is_reentrant_concave;
const CLOSED_TRIM_READ_FIRST_DEFAULT: bool = true;
pub(crate) fn closed_trim_read_first_on() -> bool {
switch!(live "BREP_BLEND_CLOSED_TRIM_READ_FIRST", CLOSED_TRIM_READ_FIRST_DEFAULT)
}
const CLOSED_WALL_PREDICT_DEFAULT: bool = true;
pub(crate) fn closed_wall_predict_on() -> bool {
switch!(live "BREP_BLEND_CLOSED_WALL_PREDICT", CLOSED_WALL_PREDICT_DEFAULT)
}
pub(crate) use corner::{open_mixed_corner_on, round_mixed_concave_open_corner};
pub(crate) mod taper_runout;
pub(crate) mod relief;