Compare commits

...

4 Commits

Author SHA1 Message Date
bherbruck 6d01515911 implicit, torus: evaluates_as's bound derived term by term; the recognised torus's sweep frame from its form (#3185, #3199) (#13743)
Moved from tangency-labs/solidcore#3224.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:19:58 -05:00
bherbruck 8ffb97d541 convert: flat parity pins the bodies that differ, not the corpus size (#13999)
The corpus grows as cases are added, so the merged kernel -> main sync saw
1,337 bodies instead of 1,332 (all five new ones identical) and failed.
Every differing body is already pinned by name; the totals now check that
the rest are identical or within rounding.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 00:27:13 -05:00
bherbruck 8f13e948a9 query + ops: whether two bodies meet, on a certified bracket of their boundary distance (#3144) (#13554)
query::contact decides whether two bodies meet from a certified bracket [lower, upper] of the distance between their boundaries; solidcore-ops exposes it as Body::meets, meets_each and boundary_distance. Undecided carries the bracket and is never read as either answer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:35:56 -05:00
bherbruck 59a85a1646 blend: a circle-class round is certified on its own torus (#3066) (#13900)
Moved from tangency-labs/solidcore#3102.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:26:25 -05:00
17 changed files with 2461 additions and 274 deletions
+12 -14
View File
@@ -7,8 +7,8 @@
//! kind, by what it became; a number within [`ROUNDING`] of its size (relative, no floor) is
//! counted as rounding, with its count and the largest.
//!
//! On the whole corpus (1,332 bodies, at #3187): 1,256 come back bit for bit, 36 within
//! rounding (the largest 4.9e-16 relative), and 40 differ, each in exactly the classes
//! On the whole corpus (1,333 bodies, at #3199 with kernel case 11991 (#3231) in): 1,259 come
//! back bit for bit, 36 within rounding (the largest 4.9e-16 relative), and 38 differ, each in exactly the classes
//! [`pinned`] lists for it.
#![allow(
clippy::unwrap_used,
@@ -390,8 +390,8 @@ fn pinned() -> BTreeMap<String, BTreeSet<String>> {
],
),
// a label its form is not proved on goes as the form alone (#2540): cylinders whose
// frames are about 1e-14 rad off their forms (two of them at the rounding of a derived
// bound, #3185's thread)
// frames are off their forms past the rounding `evaluates_as` derives (margins 1.23 to
// 2.03, tests/issue_3185_label_margins.rs)
(
label("cylinder -> rational_spline", &["radius"]),
&[
@@ -401,18 +401,14 @@ fn pinned() -> BTreeMap<String, BTreeSet<String>> {
"fillets/2903-original-rim-round.json:b0",
],
),
// tori whose frame and fitted sweep are about 1e-14 rad off their forms, rightly
// refused (#3185)
// a torus label whose frame is off its form's own first sweep row past the rounding
// `evaluates_as` derives (margin 1.69, tests/issue_3185_label_margins.rs)
(
label(
"torus -> rational_spline",
&["major_radius", "minor_radius"],
),
&[
"sweeps/3897-sweep-ring-along-a-short-arc-joined-to-a-collar.json:b6",
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub.json:b46",
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub.json:b85",
],
&["sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub.json:b46"],
),
// an extruded circle the flat hop left a spline, proved a cylinder
(
@@ -582,10 +578,12 @@ fn parity(stride: usize) {
missing.is_empty(),
"pinned bodies that no longer differ: {missing:#?}"
);
// The corpus grows as cases are added, so the totals aren't pinned: every body that
// differs is pinned above, and the rest must be identical or within rounding.
assert_eq!(
(t.bodies, t.identical, t.rounding_only),
(1332, 1256, 36),
"bodies, identical, within rounding"
(t.bodies - t.identical - t.rounding_only, t.rounding_only),
(pinned.len(), 36),
"bodies that differ, bodies within rounding"
);
// the largest rounding difference, relative (#3137: 4.885e-16)
assert!(t.rounding_largest <= 4.9e-16, "{:e}", t.rounding_largest);
@@ -0,0 +1,258 @@
//! (#3185) An analytic label is kept on its NURBS form when the kernel's analytic surface,
//! carrying the label's numbers, evaluates as the form within the rounding
//! `AnalyticSupport::build` derives term by term (`surface::implicit`'s `evaluates_as`):
//! [`AnalyticSupport::parametrisation_margin`] is the largest ratio of distance to bound over
//! the form's Greville points, at most 1 where the label stands.
//!
//! - The corpus labels #3137's parity run refused under the old constant `gamma(16 (p + q +
//! 2)) size`, each pinned to its margin as measured under the derived bound: the three tori
//! of b6 and b85 (0.02 to 0.50) stand; b46's torus (1.69) and the four cylinders (b94 1.23,
//! b43 1.40, the 2903 rims 1.52 and 2.03) do not: their frames are off their forms past
//! the rounding at their size. Since #3199 the kernel's recognised sweep frame is the form's
//! own (its end meridians), so a label is measured against the form, not against a fit
//! that shared the label's error.
//! - A torus the kernel makes at the groove's sizes stands on its own form, and the same label
//! turned about its axis by four times what the bound allows is refused.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::print_stderr,
clippy::disallowed_methods
)] // test (the turn's cos and sin)
use std::fmt::Write as _;
use std::io::Read as _;
use std::path::{Path, PathBuf};
use serde_json::Value;
use solidcore::blend::fillet_section::station::as_b_spline;
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::extent2d::Extent2d;
use solidcore::geom::vector3d::{Point3, Vec3};
use solidcore::kernel::{Kernel, KernelOptions, UpstreamFixes};
use solidcore::surface::from_parameters::from_parameters;
use solidcore::surface::implicit::AnalyticSupport;
use solidcore::surface::surface::{Surface, SurfaceKind};
use solidcore::surface::torus::{Torus, TorusOptions};
use solidcore_record::brep::Brep as Record;
fn corpus() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../solidcore/tests/kernel_cases")
}
/// The corpus's kernel, its fixes on (as the parity run and SolidCraft run it).
fn kernel() -> Kernel {
let v: Value =
serde_json::from_str(&std::fs::read_to_string(corpus().join("fixes.json")).unwrap())
.unwrap();
let mut fixes = UpstreamFixes::default();
for flag in v.as_array().unwrap() {
*fixes.flag_mut(flag.as_str().unwrap()).unwrap() = true;
}
let mut k = Kernel::new(&KernelOptions::default());
k.fixes = fixes;
k
}
fn body(case: &str, id: &str) -> Record {
let gz = corpus().join(format!("{case}.bodies.json.gz"));
let mut text = String::new();
flate2::read::GzDecoder::new(std::fs::File::open(&gz).unwrap())
.read_to_string(&mut text)
.unwrap();
let records: Value = serde_json::from_str(&text).unwrap();
serde_json::from_value(records[id].clone()).unwrap()
}
fn placement(f: &solidcore::eval::Frame) -> Axis2Placement {
let v = |a: [f64; 3]| Vec3::new(a[0], a[1], a[2]);
Axis2Placement::from_stored(
Point3::new(f.origin[0], f.origin[1], f.origin[2]),
v(f.x_axis),
v(f.y_axis),
)
}
/// A corpus face's label as the kernel's support, and its NURBS form.
fn label_and_form(
k: &Kernel,
case: &str,
id: &str,
face: usize,
) -> (
AnalyticSupport,
solidcore::nurbs::b_spline_surface::BSplineSurface,
) {
let r = body(case, id);
let f = &r.faces[face];
let nurbs = solidcore_convert::surface(f.nurbs.as_ref().unwrap()).unwrap();
let (s, _) = from_parameters(k, &nurbs, f.domain).unwrap();
let SurfaceKind::BSpline(form) = s.kind() else {
panic!("{case} {id} {face}: the form is not a B-spline")
};
let support = match solidcore_convert::surface(&f.surface).unwrap() {
solidcore::eval::Surface::Torus {
frame,
major_radius,
minor_radius,
..
} => AnalyticSupport::Torus {
placement: placement(&frame),
major_radius,
minor_radius,
},
solidcore::eval::Surface::Cylinder { frame, radius } => AnalyticSupport::Cylinder {
placement: placement(&frame),
radius,
},
other => panic!("{case} {id} {face}: {other:?}"),
};
(support, form.copy())
}
/// (case, body, face, the margin measured at #3185, whether the label stands)
const CASES: &[(&str, &str, usize, f64, bool)] = &[
(
"sweeps/3897-sweep-ring-along-a-short-arc-joined-to-a-collar",
"b6",
1,
0.0201,
true,
),
(
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub",
"b46",
1,
1.6943,
false,
),
(
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub",
"b85",
15,
0.4995,
true,
),
(
"sweeps/4280-circular-pattern-of-sweep-cut-groove-on-hub",
"b85",
21,
0.4245,
true,
),
(
"booleans/3456-deep-cut-through-drilled-taper-leaves-base",
"b94",
3,
1.2339,
false,
),
(
"crashes/2243-step-export-flanged-enclosure",
"b43",
1,
1.4008,
false,
),
("fillets/2903-original-rim-chamfer", "b0", 1, 1.5243, false),
("fillets/2903-original-rim-chamfer", "b0", 3, 2.0275, false),
("fillets/2903-original-rim-round", "b0", 1, 1.5243, false),
("fillets/2903-original-rim-round", "b0", 3, 2.0275, false),
];
/// Each corpus label's margin, pinned to 1 % of what #3185 measured, on its side of 1, and
/// `build` agreeing with it.
#[test]
fn the_corpus_labels_stand_or_fall_by_the_derived_bound() {
let k = kernel();
let mut report = String::new();
let mut wrong = Vec::new();
for &(case, id, face, pinned, stands) in CASES {
let (support, form) = label_and_form(&k, case, id, face);
let m = support.parametrisation_margin(&k, &form).unwrap().unwrap();
let _ = writeln!(report, "{case} {id} face {face}: margin {m:.4}");
let built = support.build(&k, form).is_ok();
if (m <= 1.0) != stands || built != stands || (m - pinned).abs() > 0.01 * pinned {
wrong.push(format!(
"{case} {id} face {face}: margin {m} (pinned {pinned}, stands {stands}), build {built}"
));
}
}
eprintln!("{report}");
assert!(wrong.is_empty(), "{wrong:#?}");
}
/// A torus at the groove's sizes (#3185's b46: centre (19.375, -33.558, 37.5), R 33.657, r 2,
/// a 43.6 degree sweep of the whole tube), made by the kernel: its own label stands on its
/// form; turned about its axis by four times the turn the bound allows (the margin is linear
/// in a small turn: a turn of `t` reads `m(t)`, so `4 t / m(t)` reads 4), it is refused.
#[test]
fn a_torus_label_turned_past_its_bound_is_refused() {
let k = kernel();
let o = Point3::new(19.374_999_999_999_975, -33.558_484_396_647_01, 37.5);
let x = Vec3::new(
-0.427_099_277_807_218_7,
0.739_757_649_038_078_7,
-0.519_946_946_895_745_4,
);
let y = Vec3::new(
0.259_973_473_447_872_36,
-0.450_287_264_631_874_2,
-0.854_198_555_614_438_5,
);
let domain = Extent2d::from_bounds(0.0, -180.0, 43.602_818_972_703_645, 180.0);
let made = Torus::new(
&k,
o,
x,
y,
domain,
33.657_280_044_590_65,
2.0,
TorusOptions::default(),
)
.unwrap();
let s = Surface::from(made);
let form = as_b_spline(&k, &s).unwrap().unwrap().into_owned();
let own = AnalyticSupport::of(&s).unwrap();
let m0 = own.parametrisation_margin(&k, &form).unwrap().unwrap();
assert!(m0 <= 1.0, "own label: {m0}");
assert!(own.build(&k, form.copy()).is_ok());
let turned = |t: f64| {
let AnalyticSupport::Torus {
placement,
major_radius,
minor_radius,
} = own
else {
panic!()
};
let (c, sn) = (t.cos(), t.sin());
let (px, py) = (placement.x_axis(), placement.y_axis());
AnalyticSupport::Torus {
placement: Axis2Placement::from_stored(
placement.origin(),
px * c + py * sn,
py * c - px * sn,
),
major_radius,
minor_radius,
}
};
let probe = 1e-11;
let m1 = turned(probe)
.parametrisation_margin(&k, &form)
.unwrap()
.unwrap();
assert!(m1 > 1.0, "a turn of {probe} rad reads {m1}");
let four = 4.0 * probe / m1;
let m4 = turned(four)
.parametrisation_margin(&k, &form)
.unwrap()
.unwrap();
eprintln!("own {m0:.4}; turn {probe:e}: {m1:.3}; turn {four:e}: {m4:.3}");
assert!(m4 > 1.0, "a turn of {four:e} rad reads {m4}");
assert!(turned(four).build(&k, form).is_err());
}
+140
View File
@@ -0,0 +1,140 @@
//! Contact between bodies: whether two bodies meet (share material or touch within a
//! tolerance) and the distance between their boundaries, each decided by the kernel's
//! certified bracket ([`solidcore::query::contact`]). Ops passes the tolerance on as the
//! caller gives it and reads the kernel's verdict; it computes no distance of its own.
use solidcore::geom::certified::Verdict;
use solidcore::kernel::Kernel;
use solidcore::query::contact::{self as c, Contact, Prepared};
use solidcore::topology::brep::p3::brep_copy;
use solidcore::topology::Brep;
use solidcore::Point3;
use crate::body::Body;
use crate::session::{run_on, Answer};
use crate::{Error, Result};
impl Answer for Contact {}
impl Answer for Vec<Contact> {}
/// The bracket of the distance between two bodies' boundaries, at most the width asked.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Gap {
/// No point of either boundary is nearer the other than this.
pub lower: f64,
/// The distance of the witness points, rounded up.
pub upper: f64,
/// A point on each body `upper` apart, when one is known (the first on this body).
pub witness: Option<[[f64; 3]; 2]>,
}
fn array(p: Point3) -> [f64; 3] {
[p.x, p.y, p.z]
}
/// A copy of `body`'s kernel body, the body released at once: the contact test takes its
/// operands mutably (for its classifications' caches) and never edits the caller's, and a
/// body tested against itself is locked only once at a time.
fn copy_of(k: &mut Kernel, body: &Body) -> solidcore::Result<Brep> {
let b = body.lock();
brep_copy(k, &b)
}
/// The verdict as an answer: `Yes` meets, `No` does not, `Undecided` refused with the
/// bracket, which names the tolerance that would decide.
fn decided(contact: &Contact, other: usize) -> Result<bool> {
match contact.verdict {
Verdict::Yes => Ok(true),
Verdict::No => Ok(false),
Verdict::Undecided => Err(Error::ContactUndecided {
other,
lower: contact.lower,
upper: contact.upper,
}),
}
}
fn poisoned(bodies: &[&Body]) -> Result<()> {
if bodies.iter().any(|b| b.is_poisoned()) {
return Err(Error::Internal(
"contact: a body was left unfinished by an earlier panic".to_owned(),
));
}
Ok(())
}
impl Body {
/// Whether this body and `other` meet at `tol` (the caller's: ops invents none): share
/// material, or have boundaries within `tol` of each other
/// ([`solidcore::query::contact::meets`]). Neither body changes; `other` may be this body.
///
/// # Errors
/// [`Error::ContactUndecided`] when the kernel's bracket does not decide it (`other` 0):
/// the bracket, never read as either answer; [`Error::Status`] `SM_ERR_INVALID_INPUT` for a
/// tolerance not finite and positive or a body without faces; those of the session.
pub fn meets(&self, other: &Body, tol: f64) -> Result<bool> {
poisoned(&[self, other])?;
let contact = run_on("contact::meets", &[("a", self), ("b", other)], |k| {
let mut a = copy_of(k, self)?;
let mut b = copy_of(k, other)?;
c::meets(k, &mut a, &mut b, tol)
})?;
decided(&contact, 0)
}
/// [`Body::meets`] against each of `others`, each with its own tolerance, this body
/// prepared once (its faces' pieces about the centre of its box,
/// [`Prepared::about`]).
///
/// # Errors
/// [`Error::ContactUndecided`] for the first of `others` the kernel's bracket does not
/// decide, by its index; those of [`Body::meets`].
pub fn meets_each(&self, others: &[(&Body, f64)]) -> Result<Vec<bool>> {
let mut bodies: Vec<&Body> = vec![self];
bodies.extend(others.iter().map(|(b, _)| *b));
poisoned(&bodies)?;
let inputs: Vec<(&'static str, &Body)> = bodies.iter().map(|b| ("brep", *b)).collect();
let contacts = run_on("contact::meets_each", &inputs, |k| {
let mut a = copy_of(k, self)?;
let prepared = Prepared::about(k, &a)?;
let mut contacts = Vec::new();
for (other, tol) in others {
let mut b = copy_of(k, other)?;
contacts.push(c::meets_prepared(k, &mut a, &prepared, &mut b, *tol)?);
}
Ok(contacts)
})?;
contacts
.iter()
.enumerate()
.map(|(i, contact)| decided(contact, i))
.collect()
}
/// The distance between this body's boundary and `other`'s, bracketed to at most `width`
/// (relative to its upper end, absolute below 1;
/// [`solidcore::query::contact::boundary_distance`]).
///
/// # Errors
/// [`Error::ContactUndecided`] (`other` 0) with the narrowest bracket reached, when the
/// kernel's work runs out before `width`; [`Error::Status`] `SM_ERR_INVALID_INPUT` for a
/// width not finite and positive or a body without faces; those of the session.
pub fn boundary_distance(&self, other: &Body, width: f64) -> Result<Gap> {
poisoned(&[self, other])?;
let contact = run_on(
"contact::boundary_distance",
&[("a", self), ("b", other)],
|k| {
let mut a = copy_of(k, self)?;
let mut b = copy_of(k, other)?;
c::boundary_distance(k, &mut a, &mut b, width)
},
)?;
decided(&contact, 0)?;
Ok(Gap {
lower: contact.lower,
upper: contact.upper,
witness: contact.witness.map(|[p, q]| [array(p), array(q)]),
})
}
}
+21
View File
@@ -134,6 +134,19 @@ pub enum Error {
/// The kernel found its own state inconsistent part way through a call, or was called
/// before the preparation it needs.
Exception,
/// A contact test the kernel's certified bracket of the boundary distance does not decide
/// (`solidcore::query::contact`): the tolerance lies inside `[lower, upper]` (or the
/// boundaries are apart but one body's enclosure of the other is not decided), or a
/// distance's bracket is still wider than asked. Never read as either answer. `other` is
/// the index of the body tested among the call's others.
ContactUndecided {
/// The other body's index among the call's (0 for one).
other: usize,
/// No point of either boundary is nearer the other than this.
lower: f64,
/// A pair of points, one on each boundary, is this far apart.
upper: f64,
},
/// The application cancelled the call.
Cancelled,
/// Ops was called from inside a kernel call or from the failure callback.
@@ -325,6 +338,14 @@ impl std::fmt::Display for Error {
),
Error::TooFar { largest } => write!(f, "too far to extend (largest {largest:?})"),
Error::Exception => write!(f, "solidcore raised an internal exception"),
Error::ContactUndecided {
other,
lower,
upper,
} => write!(
f,
"contact with body {other} is undecided: the boundary distance is in [{lower}, {upper}]"
),
Error::Cancelled => write!(f, "cancelled"),
Error::Reentered => write!(
f,
+6 -2
View File
@@ -19,8 +19,8 @@
//! - [`provenance`]: the hook an application's construction history rides on.
//!
//! The families: [`measure`] (mass properties, area, box, point class, gap samples, the picks'
//! distances and normals), [`boolean`], [`stitch`], [`prism`] (planar region booleans and
//! extrusion, prism sections).
//! distances and normals), [`contact`] (whether bodies meet, their boundary distance),
//! [`boolean`], [`stitch`], [`prism`] (planar region booleans and extrusion, prism sections).
//!
//! Operations change the body they are given, as the kernel does. An application that keeps
//! its input copies it first ([`Body::copy`]).
@@ -32,6 +32,7 @@ pub mod blend;
pub mod body;
pub mod boolean;
pub mod check;
pub mod contact;
pub mod error;
pub mod extend;
pub mod file;
@@ -56,6 +57,7 @@ pub use blend::{
pub use body::{make_box, make_cone, make_cylinder, make_sphere, make_torus, Body};
pub use boolean::BooleanOp;
pub use check::{CheckLevel, CheckReport};
pub use contact::Gap;
pub use error::{Error, FitKind, Result, RoomReason, Unblendable};
pub use measure::{
point_distance, Distance, EdgePoint, FaceCentroid, GapSample, MassPropertiesEstimate, Normal,
@@ -65,6 +67,8 @@ pub use offset::{draft, offset_faces, offset_sheet, shell, thicken};
pub use order::Order;
pub use step_assembly::{placed_bodies, PlacedBody};
#[cfg(test)]
mod noisy_round_tests;
#[cfg(test)]
mod test_corpus;
#[cfg(test)]
+199
View File
@@ -0,0 +1,199 @@
//! #3066: a round on supports read from a record with rounding in their frames keeps its exact
//! definition. The class is decided within the body's tolerance; the round lies on a torus of
//! its own, its net certified there at bound 0, and that torus's distance from the class's
//! torus from the supports is held to the body's tolerance, so the round answers
//! `procedural_blend()`.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::disallowed_methods
)] // test
use std::f64::consts::PI;
use solidcore::surface::surface::SurfaceKind;
use solidcore::topology::face::p2 as face;
use solidcore_record::brep::Brep as Record;
use crate::blend::{blend, Blend, Corners};
use crate::boolean::boolean;
use crate::{make_box, make_cylinder, session, Body, BooleanOp, CheckLevel};
fn fixes() {
let fixes: Vec<String> = serde_json::from_str(include_str!(
"../../solidcore/tests/kernel_cases/fixes.json"
))
.unwrap();
session::set_fixes(fixes.iter().map(String::as_str)).unwrap();
}
/// The faces of `b` that are no plane, as (with a definition, without one).
fn definitions(b: &Body) -> (usize, usize) {
let brep = b.lock();
let (mut with, mut without) = (0, 0);
for &f in brep.face_list() {
let s = face::surface(&brep, f).unwrap().unwrap();
if matches!(s.kind(), SurfaceKind::Plane(_)) {
continue;
}
if s.procedural_blend().is_some() {
with += 1;
} else {
without += 1;
}
}
(with, without)
}
/// The edge of `b` through `p`.
fn edge_through(b: &Body, p: [f64; 3]) -> usize {
let k = solidcore::Kernel::default();
let brep = b.lock();
let q = solidcore::Vec3::new(p[0], p[1], p[2]);
brep.edge_list()
.iter()
.position(|&e| {
solidcore::query::api_queries::edge_closest_point(&k, &brep, e, q)
.unwrap()
.closest
.distance
< 1e-6
})
.unwrap()
}
/// A boss R 8 standing 10 out of a 40 x 40 x 10 block, its axis turned 1e-10 off square to the
/// block's top, its root rounded 2: a circle-class round on supports only that close to the
/// class (the error the 1e-10 turn makes over the part is far inside the body's tolerance, and
/// far outside the rounding the net is certified to). The round is concave: the corner between
/// the wall and the top, less the quarter disc, turned about the axis. By Pappus, it adds
/// 2 pi (R + x) r^2 (1 - pi / 4), x = r (10 - 3 pi) / (12 - 3 pi) the corner's centroid from the
/// wall.
#[test]
fn a_round_on_a_boss_tilted_off_the_class_keeps_its_definition() {
fixes();
let block = make_box([0.0; 3], [40.0, 40.0, 10.0]).unwrap();
// the boss from z = 5 to 20, its axis turned 1e-10 about x off square to the block's top
let boss = make_cylinder([0.0; 3], 8.0, 15.0).unwrap();
let (c, s) = (1e-10_f64.cos(), 1e-10_f64.sin());
boss.transform(&[
[1.0, 0.0, 0.0, 0.0],
[0.0, c, s, 0.0],
[0.0, -s, c, 0.0],
[20.0, 20.0, 5.0, 1.0],
])
.unwrap();
let b = boolean(block, boss, BooleanOp::Union).unwrap().unwrap();
let e = edge_through(&b, [28.0, 20.0, 10.0]);
let (r, big_r) = (2.0, 8.0);
blend(&b, Blend::Round, &[e], &[r], &[], Corners::Filled).unwrap();
let report = b.check(CheckLevel::All).unwrap();
assert!(report.is_valid(), "{}", report.text);
// the boss's wall and the round: the round keeps its definition
assert_eq!(definitions(&b), (1, 1));
let x = r * (10.0 - 3.0 * PI) / (12.0 - 3.0 * PI);
let want =
16000.0 + PI * big_r * big_r * 10.0 + 2.0 * PI * (big_r + x) * r * r * (1.0 - PI / 4.0);
let got = b.volume(1e-10).unwrap();
assert!((got - want).abs() <= 1e-9 * want, "{got} {want}");
}
/// SolidCraft's raked wedge (`tests/data/raked_wedge_noisy_cylinders.json`): its end outline's
/// rounds keep their definitions, the two circle-class rounds on the read-back cylinders among
/// them; the operand's own faces, read from the record, have none.
#[test]
fn the_raked_wedges_rounds_keep_their_definitions() {
fixes();
let record: Record = serde_json::from_str(include_str!(
"../tests/data/raked_wedge_noisy_cylinders.json"
))
.unwrap();
let (b, _) = Body::from_record(&record, true).unwrap();
let (_, before) = definitions(&b);
let top = 60.0 - 60.0 * 10f64.to_radians().tan();
let mut seeds = Vec::new();
for x in [-69.0, 69.0] {
for p in [
[x, -41.0, 22.0],
[x, 8.0, 41.0],
[x, (38.0 + top) / 2.0, 60.0],
] {
seeds.push(edge_through(&b, p));
}
}
let chain = crate::blend::tangent_chain(&b, &seeds, 0.5_f64.to_radians()).unwrap();
blend(&b, Blend::Round, &chain, &[0.854], &[], Corners::Filled).unwrap();
let report = b.check(CheckLevel::All).unwrap();
assert!(report.is_valid(), "{}", report.text);
let (with, without) = definitions(&b);
// as many of the new rounds keep their definitions as on the same wedge with exact
// cylinders, and the rest are as bare there
let (exact_with, exact_bare) = exact_wedge_rounds();
assert_eq!((with, without - before), (exact_with, exact_bare));
assert_eq!(with + without - before, chain.len());
}
/// The same wedge built here, its cylinders exact, its end outline rounded as SolidCraft's: of the
/// rounds that last call makes, how many keep a definition and how many don't.
fn exact_wedge_rounds() -> (usize, usize) {
use crate::sweep::{extrude, Curve, DraftCorner};
let top = 60.0 - 60.0 * 10f64.to_radians().tan();
let profile = [
[-60.0, 0.0],
[60.0, 0.0],
[top, 60.0],
[38.0, 60.0],
[-22.0, 22.0],
[-60.0, 22.0],
];
let at = |p: [f64; 2]| [-69.0, p[0], p[1]];
let curves: Vec<Curve> = (0..6)
.map(|i| Curve::Line {
start: at(profile[i]),
end: at(profile[(i + 1) % 6]),
})
.collect();
let b = extrude(
&curves,
[1.0, 0.0, 0.0],
138.0,
0.0,
DraftCorner::Curvature,
true,
)
.unwrap();
let round = |pts: &[[f64; 3]], r: f64, chain: bool| {
let seeds: Vec<usize> = pts.iter().map(|&p| edge_through(&b, p)).collect();
let edges = if chain {
crate::blend::tangent_chain(&b, &seeds, 0.5_f64.to_radians()).unwrap()
} else {
seeds
};
blend(&b, Blend::Round, &edges, &[r], &[], Corners::Filled).unwrap();
};
round(&[[0.0, -60.0, 22.0]], 1.2, true);
round(&[[0.0, -22.0, 22.0]], 1.5, true);
round(&[[0.0, 38.0, 60.0]], 1.5, true);
let back = (60.0 + top) / 2.0;
let four = [
[-69.0, -60.0, 12.0],
[69.0, -60.0, 12.0],
[-69.0, back, 30.0],
[69.0, back, 30.0],
];
round(&four, 4.0, false);
let (with_before, bare_before) = definitions(&b);
let mut ends = Vec::new();
for x in [-69.0, 69.0] {
ends.extend([
[x, -41.0, 22.0],
[x, 8.0, 41.0],
[x, (38.0 + top) / 2.0, 60.0],
]);
}
round(&ends, 0.854, true);
let (with, bare) = definitions(&b);
(with - with_before, bare - bare_before)
}
+95
View File
@@ -0,0 +1,95 @@
//! Contact between bodies on closed forms: meets, meets against several, the boundary
//! distance, and the refusals.
#![allow(clippy::unwrap_used)]
use solidcore_ops::session;
use solidcore_ops::{make_box, make_sphere, Error};
fn all_fixes() {
session::set_thread_fixes(solidcore::kernel::UpstreamFixes::NAMES.iter().copied()).unwrap();
}
#[test]
fn boxes_sharing_a_face_meet() {
all_fixes();
let a = make_box([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let b = make_box([1.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
assert!(a.meets(&b, 1e-6).unwrap());
assert!(b.meets(&a, 1e-6).unwrap());
}
#[test]
fn boxes_one_apart_do_not_meet_and_their_distance_is_one() {
all_fixes();
let a = make_box([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let b = make_box([2.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
assert!(!a.meets(&b, 1e-3).unwrap());
let g = a.boundary_distance(&b, 1e-9).unwrap();
assert!(g.lower <= 1.0 && 1.0 <= g.upper, "{g:?}");
assert!(g.upper - g.lower <= 1e-9 * g.upper.max(1.0), "{g:?}");
let [p, q] = g.witness.unwrap();
assert!(
(p[0] - 1.0).abs() <= 1e-12 && (q[0] - 2.0).abs() <= 1e-12,
"{g:?}"
);
}
#[test]
fn a_body_meets_itself_and_one_it_encloses() {
all_fixes();
let a = make_box([0.0, 0.0, 0.0], [3.0, 3.0, 3.0]).unwrap();
let inner = make_box([1.0, 1.0, 1.0], [1.0, 1.0, 1.0]).unwrap();
assert!(a.meets(&a, 1e-6).unwrap());
assert!(a.meets(&inner, 1e-6).unwrap());
assert!(inner.meets(&a, 1e-6).unwrap());
// neither body changes
assert_eq!(a.counts().unwrap(), [6, 12, 8]);
assert_eq!(inner.counts().unwrap(), [6, 12, 8]);
}
#[test]
fn meets_each_answers_each_with_its_own_tolerance() {
all_fixes();
let a = make_sphere([0.0, 0.0, 0.0], 1.0).unwrap();
let touching = make_box([1.0, -1.0, -1.0], [1.0, 2.0, 2.0]).unwrap();
let near = make_sphere([2.5, 0.0, 0.0], 1.0).unwrap(); // 0.5 apart
let far = make_box([5.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let got = a
.meets_each(&[(&touching, 1e-6), (&near, 0.1), (&near, 0.6), (&far, 1.0)])
.unwrap();
assert_eq!(got, vec![true, false, true, false]);
}
/// The refusal is the kernel's own bracket: a tolerance equal to a gap the kernel brackets
/// to its rounding is inside the bracket, and neither answer is given.
#[test]
fn a_tolerance_inside_the_bracket_is_refused_with_it() {
all_fixes();
let a = make_box([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let b = make_box([2.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let far = make_box([5.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
match a.meets(&b, 1.0) {
Err(Error::ContactUndecided {
other: 0,
lower,
upper,
}) => assert!(lower <= 1.0 && 1.0 <= upper, "[{lower}, {upper}]"),
r => panic!("{r:?}"),
}
match a.meets_each(&[(&far, 1e-3), (&b, 1.0)]) {
Err(Error::ContactUndecided { other: 1, .. }) => {}
r => panic!("{r:?}"),
}
}
#[test]
fn a_tolerance_or_width_not_finite_and_positive_is_refused() {
all_fixes();
let a = make_box([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let b = make_box([2.0, 0.0, 0.0], [1.0, 1.0, 1.0]).unwrap();
let invalid = Error::Status(solidcore::error::SM_ERR_INVALID_INPUT);
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert_eq!(a.meets(&b, bad).unwrap_err(), invalid, "{bad}");
assert_eq!(a.boundary_distance(&b, bad).unwrap_err(), invalid, "{bad}");
}
}
+49 -58
View File
@@ -2521,46 +2521,11 @@ fn keep_definition(
let Some(surface) = pseudo.geom_mut(g)?.fillet_surface_mut(None)? else {
return Err(SolidError::null_pointer(OP));
};
keep_round_if_proved(k, surface, round, inputs.tol)?;
round.recertify(k, surface)?;
surface.set_exact_round(Some(round));
Ok(())
}
/// `round` kept on `net` where it is proved ([`ExactRound::recertify`]), returning whether it
/// was. The class is read within `tol`, the body's tolerance, and the certificate to rounding.
/// Supports only that close to the class (a cylinder's axis 1e-13 off square to a plane, as
/// read from a record) make a net that is the round but not provably on the class's surface:
/// no bound of 0, so it keeps no definition. A net further off the
/// class than `tol` ([`ExactRound::miss`]) is built wrongly, and stays an error.
///
/// # Errors
///
/// The certificate's refusal of a net more than `tol` off the class, or of supports that
/// give the class no surface; those of the certificate and of [`ExactRound::miss`].
pub fn keep_round_if_proved(
k: &Kernel,
net: &mut Surface,
round: ExactRound,
tol: f64,
) -> Result<bool> {
match round.recertify(k, net) {
Ok(_) => {
net.set_exact_round(Some(round));
Ok(true)
}
Err(e) => {
let off = matches!(
&e,
SolidError::InvalidInput { operation, .. }
if *operation == crate::surface::implicit::OP_EXACT
);
match off.then(|| round.miss(k, net)).transpose()?.flatten() {
Some(miss) if miss <= tol => Ok(false),
_ => Err(e),
}
}
}
}
/// [`calc_exact_fillet_geom`]'s build by the class (`CalcExactFilletGeom`), without the
/// definition kept.
#[allow(clippy::too_many_arguments)]
@@ -2719,7 +2684,8 @@ fn exact_fillet_geom_of_class(
mod tests {
use super::*;
/// A box's edge rounded 2: the round's net, without its definition, and the definition.
/// A cylinder R 10, 20 high, its top rim rounded 2 (a round of the circle class): the
/// round's net, without its definition, and the definition.
fn kept_round() -> (Kernel, Surface, ExactRound) {
use crate::blend::fillet_executive::session::FilletSession;
use crate::kernel::UpstreamFixes;
@@ -2729,24 +2695,51 @@ mod tests {
fixes: UpstreamFixes::all(),
..Kernel::default()
};
let mut b = crate::primitive::api_primitives::create_box(
let mut b = crate::primitive::api_primitives::create_cylinder(
&mut k,
Point3::new(0.0, 0.0, 0.0),
30.0,
20.0,
10.0,
20.0,
)
.unwrap();
let edge = b.edge_list()[0];
// the top rim: the edge through (10, 0, 20)
let rim = *b
.edge_list()
.iter()
.min_by(|&&x, &&y| {
let d = |e| {
crate::query::api_queries::edge_closest_point(
&k,
&b,
e,
Point3::new(10.0, 0.0, 20.0),
)
.unwrap()
.closest
.distance
+ crate::query::api_queries::edge_closest_point(
&k,
&b,
e,
Point3::new(-10.0, 0.0, 20.0),
)
.unwrap()
.closest
.distance
};
d(x).total_cmp(&d(y))
})
.unwrap();
{
let mut s = FilletSession::new(&mut k, &mut b).unwrap();
s.prepare_constant_radius_edges(&mut k, &[edge], 2.0, 1, 1, 1.0)
s.prepare_constant_radius_edges(&mut k, &[rim], 2.0, 1, 1, 1.0)
.unwrap();
s.do_fillet(&mut k).unwrap();
}
for &f in b.face_list() {
let s = face::surface(&b, f).unwrap().unwrap();
if let Some(BlendRef::Exact { round, .. }) = s.procedural_blend() {
assert_eq!(round.class, ExactClass::Circle);
let round = round.clone();
let mut net = s.clone();
net.set_exact_round(None);
@@ -2769,23 +2762,21 @@ mod tests {
net
}
/// The class is read within 2e-5 here. A net on it keeps its definition; one off it by
/// less keeps none (the round of supports that close to the class); one further off is
/// built wrongly, and stays an error.
/// A circle-class round's definition is the torus its net is on, a stated distance from
/// the supports' class torus within the tolerance the class was read at (2e-5 here): the
/// net as built is at 0, the net moved by (1, 1, 1) 1e-7 is at sqrt 3 1e-7, and the net
/// moved by (1, 1, 1) 1e-3 is refused.
#[test]
fn a_net_off_its_class_keeps_no_definition_within_the_tolerance_only() {
let tol = 2e-5;
fn a_circle_rounds_net_is_certified_on_its_own_torus_near_the_class() {
let (k, net, round) = kept_round();
let mut on = net.clone();
assert!(keep_round_if_proved(&k, &mut on, round.clone(), tol).unwrap());
assert!(on.procedural_blend().is_some());
let mut near = moved(&k, &net, 1e-7);
assert!(!keep_round_if_proved(&k, &mut near, round.clone(), tol).unwrap());
assert!(near.procedural_blend().is_none());
let mut far = moved(&k, &net, 1e-3);
let refused = keep_round_if_proved(&k, &mut far, round, tol);
assert_eq!(round.recertify(&k, &net).unwrap(), 0.0);
assert!(round.supports_distance(&k, &net).unwrap() < 1e-12);
let near = moved(&k, &net, 1e-7);
assert_eq!(round.recertify(&k, &near).unwrap(), 0.0);
let d = round.supports_distance(&k, &near).unwrap();
assert!((d - 3.0_f64.sqrt() * 1e-7).abs() < 1e-12, "{d}");
let far = moved(&k, &net, 1e-3);
let refused = round.recertify(&k, &far);
assert!(
matches!(refused, Err(SolidError::InvalidInput { .. })),
"{refused:?}"
@@ -1304,7 +1304,7 @@ fn rational_spine_parts(
#[cfg(test)]
mod tests {
use super::*;
use crate::blend::fillet_exact::{classify_supports, keep_round_if_proved, ClassInputs};
use crate::blend::fillet_exact::{classify_supports, ClassInputs};
use crate::curve::curve::CurveKind;
use crate::geom::axis2_placement::Axis2Placement;
use crate::surface::cone::Cone;
@@ -1779,10 +1779,10 @@ mod tests {
inputs,
)
.unwrap();
assert!(
keep_round_if_proved(&k, &mut net, round, inputs.tol).unwrap(),
"{offsets:?}, {contact:?}"
);
if let Err(e) = round.recertify(&k, &net) {
panic!("{offsets:?}, {contact:?}: {e}");
}
net.set_exact_round(Some(round));
assert_eq!(
net.procedural_blend().unwrap().exact().unwrap().bound(),
0.0
+1
View File
@@ -6,6 +6,7 @@
pub mod api_queries;
pub mod brep;
pub mod contact;
pub mod distance_bound;
pub mod edge;
pub mod edge_turn;
+732
View File
@@ -0,0 +1,732 @@
//! Not upstream: whether two bodies meet (share material or touch within a tolerance),
//! decided only where it is proved: [`meets`] and its distance, [`boundary_distance`].
//!
//! | Rust | Upstream |
//! |------|----------|
//! | [`meets`], [`boundary_distance`], [`Prepared`] | none (`SmApiBrepRelationship`, ported as [`crate::query::api_queries::brep_relationship`], is a Newton solve: its gap is a point pair, never a lower bound, and its containment counts samples) |
//!
//! **The bracket.** The distance between the two bodies' boundaries is enclosed in `[lower,
//! upper]`:
//!
//! - `lower`: the least, over pairs of pieces of the two bodies' faces, of the distance
//! between their hulls' boxes. The pieces are those of
//! [`crate::query::distance_bound`] (each face's NURBS form over its UV box, its Bézier
//! patches about one common point, every coefficient's error carried and each box widened
//! by it), so no point of either boundary is nearer the other than `lower`. Pairs wait on a
//! heap, nearest first; the nearest is split (its larger piece, in four), so `lower`
//! rises. A piece whose UV rectangle is proved off its face (no trim curve's box meets it,
//! and its centre classifies outside the face) is dropped: a face's hole is no material.
//! - `upper` and its witness: a pair of points, one on each body, each on its face's surface
//! at parameters the face classifies decisively inside (`face_classify_uv`, the face
//! itself: neither on its boundary nor undecided), their distance rounded up. The
//! candidates are the centres of the pairs taken from the heap, each moved to the other
//! surface's closest point and back ([`crate::query::api_queries::surface_closest_point`],
//! a few times), so two surfaces that cross reach a distance of their rounding.
//!
//! **Meets.** At a tolerance `tol`: `Yes` when `upper <= tol` (with the witness); when
//! `lower > tol`, the boundaries do not touch, and the bodies meet only if one encloses the
//! other: a point of each boundary is classified in the other body, `Yes` when one is
//! inside its material, `No` when both are outside. A classification on the other's boundary
//! or not decided is no answer, and neither is a bracket that still straddles `tol` when
//! the pairs reach [`MAX_PAIRS`]: [`Verdict::Undecided`], with the bracket, which names
//! the size that would decide.
//!
//! Deviations: none (no upstream counterpart). No recursion: the pairs wait on a heap.
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::geom::certified::{gamma, Verdict};
use crate::geom::extent1d::Extent1d;
use crate::geom::tol::{brep_zone_tol_3d, SmallTopoType};
use crate::geom::vector2d::Point2;
use crate::intersect::point_class::PointClassType;
use crate::kernel::Kernel;
use crate::mass::integrator::properties::region_material;
use crate::math::Point3;
use crate::query::api_queries::{
edge_closest_point, face_bounding_box, face_classify_uv, surface_closest_point,
};
use crate::query::brep::BrepClassifier;
use crate::query::distance_bound::{pieces_about, Piece};
use crate::surface::surface::Surface;
use crate::topology::{edgeuse::p1 as eu1, loopuse, Brep, FaceId};
use crate::{EntityRef, Result, SolidError};
const OP: &str = "query::contact::meets";
/// The most piece pairs made for one answer.
pub const MAX_PAIRS: usize = 1 << 16;
/// The times a witness is moved to the other surface's closest point and back.
const PROJECTIONS: usize = 6;
/// How two bodies stand: the verdict at the tolerance asked, and the boundary distance's
/// bracket.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Contact {
/// `Yes` (they share material or their boundaries come within the tolerance), `No`, or
/// `Undecided` (the bracket straddles the tolerance at the work's limit, or a
/// classification was not decided).
pub verdict: Verdict,
/// A certified lower bound of the distance between the boundaries.
pub lower: f64,
/// An upper bound (the witness's distance, rounded up); infinite without a witness.
pub upper: f64,
/// Two points, one on each boundary, `upper` apart.
pub witness: Option<[Point3; 2]>,
}
/// A face's pieces and what decides whether a piece is on it.
struct FacePieces {
face: FaceId,
surface: Surface,
/// Its form's certified distance from its definition, lost from every bound.
slack: f64,
pieces: Vec<Piece>,
/// The boxes of its own UV trim curves `[u0, u1, v0, v1]`; `None` when a trim curve or
/// its box is missing (then no piece is dropped).
trims: Option<Vec<[f64; 4]>>,
}
/// A body made ready for contact tests: its faces' pieces about a common point.
pub struct Prepared {
origin: Point3,
faces: Vec<FacePieces>,
}
/// A pair of pieces, one of each body, with its lower bound.
struct Pair {
lower: f64,
a: (usize, Piece),
b: (usize, Piece),
}
impl PartialEq for Pair {
fn eq(&self, o: &Self) -> bool {
self.lower.total_cmp(&o.lower) == Ordering::Equal
}
}
impl Eq for Pair {}
impl PartialOrd for Pair {
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl Ord for Pair {
// the heap pops the smallest bound first
fn cmp(&self, o: &Self) -> Ordering {
o.lower.total_cmp(&self.lower)
}
}
/// The box of body `b`'s faces, `(lo, hi)`.
fn body_box(k: &Kernel, b: &Brep) -> Result<(Point3, Point3)> {
let mut lo = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut hi = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for &f in b.face_list() {
let e = face_bounding_box(k, b, f, false)?;
let (l, h) = (e.min(), e.max());
lo = Point3::new(lo.x.min(l.x), lo.y.min(l.y), lo.z.min(l.z));
hi = Point3::new(hi.x.max(h.x), hi.y.max(h.y), hi.z.max(h.z));
}
if !(lo.x <= hi.x && lo.y <= hi.y && lo.z <= hi.z) {
return Err(SolidError::invalid_input(OP, "a body without faces"));
}
Ok((lo, hi))
}
/// The common point two bodies' pieces are made about: the centre of their boxes.
fn common_origin(k: &Kernel, a: &Brep, b: &Brep) -> Result<Point3> {
let ((al, ah), (bl, bh)) = (body_box(k, a)?, body_box(k, b)?);
let mid = |x: f64, y: f64, z: f64, w: f64| 0.5 * (x.min(z) + y.max(w));
Ok(Point3::new(
mid(al.x, ah.x, bl.x, bh.x),
mid(al.y, ah.y, bl.y, bh.y),
mid(al.z, ah.z, bl.z, bh.z),
))
}
/// The parts each Bézier segment of a trim curve is cut into for its boxes: a rectangle
/// that no part's box meets is proved clear of the curve.
const TRIM_PARTS: usize = 16;
/// The boxes, `[u0, u1, v0, v1]`, of a UV trim curve's NURBS form: each Bézier segment cut in
/// [`TRIM_PARTS`] by de Casteljau on its homogeneous points, each part in the box of its
/// Euclidean points (its hull: positive weights), widened by the subdivision's rounding;
/// `None` for a form whose weights are not positive.
fn nurb_boxes(c: &crate::nurbs::crv_basic::Curve) -> Result<Option<Vec<[f64; 4]>>> {
use crate::nurbs::math::NOW;
let mut out = Vec::new();
for seg in c.decompose_bez()? {
let pts: Vec<[f64; 3]> = seg
.cpts()
.iter()
.map(|q| {
let w = if q.w == NOW { 1.0 } else { q.w };
[q.x, q.y, w]
})
.collect();
if pts
.iter()
.any(|q| !(q[2] > 0.0 && q.iter().all(|v| v.is_finite())))
{
return Ok(None);
}
let size = pts.iter().flatten().fold(0.0_f64, |m, v| m.max(v.abs()));
let wmin = pts.iter().fold(f64::INFINITY, |m, q| m.min(q[2]));
// the right part from t: de Casteljau at t on the homogeneous points
let cut = |p: &[[f64; 3]], t: f64| -> (Vec<[f64; 3]>, Vec<[f64; 3]>) {
let mut level = p.to_vec();
let (mut left, mut right) = (Vec::new(), Vec::new());
while let (Some(&f), Some(&l)) = (level.first(), level.last()) {
left.push(f);
right.push(l);
level = level
.windows(2)
.filter_map(|w| match w {
[a, b] => Some([
(1.0 - t) * a[0] + t * b[0],
(1.0 - t) * a[1] + t * b[1],
(1.0 - t) * a[2] + t * b[2],
]),
_ => None,
})
.collect();
}
right.reverse();
(left, right)
};
let degree = pts.len().saturating_sub(1);
// TRIM_PARTS levels of convex combinations at most: their rounding, of the size
let err = gamma(3 * degree * TRIM_PARTS + 3) * size;
let mut rest = pts;
for i in 0..TRIM_PARTS {
let t = 1.0 / (TRIM_PARTS - i) as f64;
let (part, next) = if i + 1 == TRIM_PARTS {
(rest.clone(), Vec::new())
} else {
cut(&rest, t)
};
rest = next;
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
for q in &part {
for (k, v) in [q[0] / q[2], q[1] / q[2]].into_iter().enumerate() {
if let (Some(l), Some(h)) = (lo.get_mut(k), hi.get_mut(k)) {
*l = l.min(v);
*h = h.max(v);
}
}
}
// a quotient of coefficients within err: within err (1 + |x|) / (w - err)
let widen = err * (1.0 + size / wmin) / (wmin - err).max(f64::MIN_POSITIVE)
+ gamma(2) * size / wmin;
out.push([lo[0] - widen, hi[0] + widen, lo[1] - widen, hi[1] + widen]);
}
}
Ok(Some(out))
}
/// The boxes of face `f`'s own UV trim curves (each its NURBS form's parts,
/// [`nurb_boxes`], or the curve's whole box where it has none); `None` where a trim curve or
/// its box is missing.
fn trim_boxes(k: &Kernel, b: &Brep, f: FaceId) -> Result<Option<Vec<[f64; 4]>>> {
use crate::curve::curve::CurveKind;
let Some(fu) = b.face(f)?.faceuse else {
return Ok(None);
};
let mut out = Vec::new();
for lu in eu1::faceuse_loopuses(b, fu, false, None)? {
for eu in loopuse::edgeuses(b, lu, None, None)? {
// its own, or its mate's on the face's other side (the same face's parameters)
let Some(c) = eu1::uv_trim_curve(b, eu)?.0 else {
return Ok(None);
};
let nurb = match c.kind() {
CurveKind::BSpline(x) => x.gw_nurb(),
CurveKind::Ellipse(x) => x.base().gw_nurb(),
CurveKind::Hyperbola(x) => x.base().gw_nurb(),
CurveKind::Parabola(x) => x.base().gw_nurb(),
CurveKind::Line(x) => x.base().gw_nurb(),
_ => None,
};
if let Some(n) = nurb {
if let Some(boxes) = nurb_boxes(n)? {
out.extend(boxes);
continue;
}
}
let interval: Extent1d = c.natural_interval();
let boxes = c.calculate_bounding_box(
k,
&interval,
crate::nurbs::b_spline_curve::BoundingBoxRequest {
normal: true,
..Default::default()
},
true,
)?;
let Some(e) = boxes.normal else {
return Ok(None);
};
let (l, h) = (e.min(), e.max());
// the box's own rounding, a few ulps of its size
let r = 16.0 * f64::EPSILON * (l.x.abs() + h.x.abs() + l.y.abs() + h.y.abs());
out.push([l.x - r, h.x + r, l.y - r, h.y + r]);
}
}
Ok(Some(out))
}
impl Prepared {
/// Body `b`'s faces' pieces about `origin`.
///
/// # Errors
///
/// `InvalidInput` for a body without faces or a face without a surface; those of the
/// pieces ([`crate::query::distance_bound`]) and the trim curves' boxes.
pub fn new(k: &Kernel, b: &Brep, origin: Point3) -> Result<Self> {
let mut faces = Vec::new();
for &f in b.face_list() {
let face = b.face(f)?;
let surface = face
.surface
.as_ref()
.ok_or_else(|| SolidError::invalid_input(OP, "a face without a surface"))?
.clone();
let (pieces, slack) = pieces_about(k, &surface, Some(&face.uv_domain), origin)?;
let trims = trim_boxes(k, b, f)?;
faces.push(FacePieces {
face: f,
surface,
slack,
pieces,
trims,
});
}
if faces.is_empty() {
return Err(SolidError::invalid_input(OP, "a body without faces"));
}
Ok(Self { origin, faces })
}
/// Body `b`'s faces' pieces about the centre of its own box: the preparation of one
/// body for tests against several ([`meets_prepared`]).
///
/// # Errors
///
/// Those of [`Prepared::new`] and of the faces' boxes.
pub fn about(k: &Kernel, b: &Brep) -> Result<Self> {
Self::new(k, b, common_origin(k, b, b)?)
}
}
/// The certified lower bound of the distance between two pieces about the same point: the
/// larger of the distance of their widened boxes and, along a unit direction `axis` (the
/// witness's, when there is one), the gap between their hulls' extents
/// ([`Piece::extent_along`]), each rounded down. The axis steers which bound is tight, never
/// whether either holds: any direction separates the hulls by at most their distance.
fn pair_lower(a: &Piece, b: &Piece, axis: Option<[f64; 3]>) -> Result<f64> {
let by_box = box_lower(a, b)?;
let Some(u) = axis else {
return Ok(by_box);
};
let len = (u[0] * u[0] + u[1] * u[1] + u[2] * u[2]).sqrt() * (1.0 + gamma(4));
if !(len > 0.0 && len.is_finite()) {
return Ok(by_box);
}
let ((a0, a1), (b0, b1)) = (a.extent_along(u)?, b.extent_along(u)?);
let gap = (b0 - a1).max(a0 - b1).max(0.0);
// (the difference rounded down by gamma_1 of its terms, then over the length)
let gap = (gap - gamma(1) * (a0.abs() + a1.abs() + b0.abs() + b1.abs())).max(0.0);
let by_axis = gap / len * (1.0 - gamma(2));
Ok(by_box.max(by_axis))
}
/// The distance of two pieces' widened boxes, rounded down.
fn box_lower(a: &Piece, b: &Piece) -> Result<f64> {
let ((al, ah), (bl, bh)) = (a.widened_box()?, b.widened_box()?);
let mut sum = 0.0_f64;
for i in 0..3 {
let (Some(&x0), Some(&x1), Some(&y0), Some(&y1)) =
(al.get(i), ah.get(i), bl.get(i), bh.get(i))
else {
return Err(SolidError::sm_err(OP));
};
let gap = (y0 - x1).max(x0 - y1).max(0.0);
// (the difference rounded up by at most gamma_1 of its terms)
let gap = (gap - gamma(1) * (x0.abs() + x1.abs() + y0.abs() + y1.abs())).max(0.0);
sum += gap * gap;
}
Ok(sum.sqrt() * (1.0 - gamma(5)))
}
/// Where a piece stands against its face: `Some(true)` proved on it and decisively inside
/// at its centre, `Some(false)` proved off it (no trim curve's box meets its rectangle and
/// its centre is outside), `None` not decided.
fn piece_on_face(k: &Kernel, b: &mut Brep, fp: &FacePieces, p: &Piece) -> Result<Option<bool>> {
let [u0, u1, v0, v1] = p.uv;
let crosses = match &fp.trims {
None => true,
Some(boxes) => boxes
.iter()
.any(|t| !(t[1] < u0 || t[0] > u1 || t[3] < v0 || t[2] > v1)),
};
let centre = Point2::new(0.5 * (u0 + u1), 0.5 * (v0 + v1));
let class = match face_classify_uv(k, b, fp.face, centre) {
Ok(c) => c,
// a classification that fails decides nothing
Err(e) if e.is_cancelled() => return Err(e),
Err(_) => return Ok(None),
};
Ok(match class {
PointClassType::Unknown if !crosses => Some(false),
PointClassType::Face => Some(true),
_ => None,
})
}
/// A point of body `b` for face `fp` at `uv`: the surface's point when the face classifies
/// `uv` decisively inside it; when it classifies it on the face's boundary (within its
/// tolerance, either side), the nearest point of the face's edges to the surface's point,
/// which is on the body whichever side `uv` was; `None` outside or not decided.
fn on_face(k: &Kernel, b: &mut Brep, fp: &FacePieces, uv: Point2) -> Result<Option<Point3>> {
match face_classify_uv(k, b, fp.face, uv) {
Ok(PointClassType::Face) => Ok(Some(fp.surface.evaluate_point(k, uv)?)),
Ok(PointClassType::Edge | PointClassType::Vertex) => {
let x = fp.surface.evaluate_point(k, uv)?;
nearest_edge_point(k, b, fp.face, x)
}
Err(e) if e.is_cancelled() => Err(e),
_ => Ok(None),
}
}
/// The nearest point to `x` of face `f`'s edges (each solved on its edge, so on the body);
/// `None` when no edge's solve succeeds.
fn nearest_edge_point(k: &Kernel, b: &Brep, f: FaceId, x: Point3) -> Result<Option<Point3>> {
let Some(fu) = b.face(f)?.faceuse else {
return Ok(None);
};
let mut best: Option<(f64, Point3)> = None;
for lu in eu1::faceuse_loopuses(b, fu, false, None)? {
for eu in loopuse::edgeuses(b, lu, None, None)? {
let Some(e) = b.edgeuse(eu)?.edge else {
continue;
};
let Ok(c) = edge_closest_point(k, b, e, x) else {
continue;
};
let d = c.closest.point.distance(x);
if best.is_none_or(|(m, _)| d < m) {
best = Some((d, c.closest.point));
}
}
}
Ok(best.map(|(_, p)| p))
}
/// The distance of two points rounded up: `|p - q|` within `gamma_5` of the terms' size.
fn distance_up(p: Point3, q: Point3) -> f64 {
let size = p.x.abs() + p.y.abs() + p.z.abs() + q.x.abs() + q.y.abs() + q.z.abs();
p.distance(q) * (1.0 + gamma(5)) + gamma(5) * size
}
/// The closest point of `fp`'s surface to `x`, kept when its face classifies it decisively
/// inside; `None` for a solve that fails or a point off the face.
fn closest_on(
k: &Kernel,
b: &mut Brep,
fp: &FacePieces,
x: Point3,
) -> Result<Option<(Point3, Point2)>> {
let Ok(c) = surface_closest_point(k, &fp.surface, x) else {
return Ok(None);
};
let Some(&uv) = c.uvs.first() else {
return Ok(None);
};
Ok(on_face(k, b, fp, uv)?.map(|p| (p, uv)))
}
/// The bracket between `a` and `b` (as [`Prepared`]), refined until `stop` says done or the
/// pairs reach [`MAX_PAIRS`]: `stop(lower, upper)`.
fn bracket(
k: &Kernel,
a: &mut Brep,
pa: &Prepared,
b: &mut Brep,
pb: &Prepared,
seed: f64,
stop: &dyn Fn(f64, f64) -> bool,
) -> Result<(f64, f64, Option<[Point3; 2]>, bool)> {
let mut heap = BinaryHeap::new();
let mut made = 0_usize;
for (ia, fa) in pa.faces.iter().enumerate() {
for (ib, fb) in pb.faces.iter().enumerate() {
for x in &fa.pieces {
for y in &fb.pieces {
let lower = (pair_lower(x, y, None)? - fa.slack - fb.slack).max(0.0);
heap.push(Pair {
lower,
a: (ia, x.clone()),
b: (ib, y.clone()),
});
made += 1;
}
}
}
}
let (mut upper, mut witness) = (f64::INFINITY, None);
// witnesses first from every face pair whose bound is within `seed`, nearest first (the
// pairs tie closely where two bodies lie along each other; each is visited once here)
let mut first: Vec<&Pair> = heap.iter().filter(|p| p.lower <= seed).collect();
first.sort_by(|x, y| x.lower.total_cmp(&y.lower));
for pair in first {
let (Some(fa), Some(fb)) = (pa.faces.get(pair.a.0), pb.faces.get(pair.b.0)) else {
return Err(SolidError::sm_err(OP));
};
if pair.lower < upper {
witness_from(
k,
(&mut *a, fa, &pair.a.1),
(&mut *b, fb, &pair.b.1),
&mut upper,
&mut witness,
)?;
}
if stop(0.0, upper) {
break;
}
}
loop {
k.check_cancelled(OP)?;
let Some(pair) = heap.pop() else {
// every piece proved off its face: no boundary is there to bound
return Ok((f64::INFINITY, upper, witness, true));
};
let lower = pair.lower.min(upper);
if stop(lower, upper) {
return Ok((lower, upper, witness, true));
}
if made >= MAX_PAIRS {
return Ok((lower, upper, witness, false));
}
let (ia, x) = &pair.a;
let (ib, y) = &pair.b;
let (Some(fa), Some(fb)) = (pa.faces.get(*ia), pb.faces.get(*ib)) else {
return Err(SolidError::sm_err(OP));
};
// a witness from the pair's centres (none can be nearer than the pair's bound, so a
// pair past the witness so far is skipped)
if pair.lower < upper {
witness_from(
k,
(&mut *a, fa, x),
(&mut *b, fb, y),
&mut upper,
&mut witness,
)?;
}
// split the larger piece
let size = |p: &Piece| -> Result<f64> {
let (l, h) = p.widened_box()?;
Ok((0..3)
.map(|i| h.get(i).copied().unwrap_or(0.0) - l.get(i).copied().unwrap_or(0.0))
.fold(0.0_f64, f64::max))
};
let split_a = size(x)? >= size(y)?;
let (from, keep) = if split_a { (x, y) } else { (y, x) };
// the witness's direction, for the hulls' extents along it
let axis = witness.map(|[p, q]| [q.x - p.x, q.y - p.y, q.z - p.z]);
for q in from.quarters()? {
let (fp, brep) = if split_a {
(fa, &mut *a)
} else {
(fb, &mut *b)
};
if piece_on_face(k, brep, fp, &q)? == Some(false) {
continue;
}
let (qa, qb) = if split_a { (&q, keep) } else { (keep, &q) };
let lower = (pair_lower(qa, qb, axis)? - fa.slack - fb.slack).max(0.0);
heap.push(Pair {
lower,
a: (*ia, qa.clone()),
b: (*ib, qb.clone()),
});
made += 1;
}
}
}
/// A witness from a pair's centres, each moved to the other's surface's closest point and
/// back ([`PROJECTIONS`] times), kept where both points are decisively on their faces and
/// nearer than `upper`.
fn witness_from(
k: &Kernel,
(a, fa, x): (&mut Brep, &FacePieces, &Piece),
(b, fb, y): (&mut Brep, &FacePieces, &Piece),
upper: &mut f64,
witness: &mut Option<[Point3; 2]>,
) -> Result<()> {
let (_, ua) = x.centre();
let (_, ub) = y.centre();
let mut pa_pt = on_face(k, a, fa, Point2::new(ua[0], ua[1]))?;
let mut pb_pt = on_face(k, b, fb, Point2::new(ub[0], ub[1]))?;
for _ in 0..PROJECTIONS {
if let Some(p) = pa_pt {
if let Some((q, _)) = closest_on(k, b, fb, p)? {
pb_pt = Some(q);
}
}
if let Some(q) = pb_pt {
if let Some((p, _)) = closest_on(k, a, fa, q)? {
pa_pt = Some(p);
}
}
if let (Some(p), Some(q)) = (pa_pt, pb_pt) {
let d = distance_up(p, q) + fa.slack + fb.slack;
if d < *upper {
*upper = d;
*witness = Some([p, q]);
}
}
}
Ok(())
}
/// A point of `inner`'s boundary classified in `outer`: `Some(true)` inside its material,
/// `Some(false)` outside it, `None` when none of `inner`'s points tried (its pieces'
/// centres, where their faces hold them: [`on_face`]) is proved more than twice the
/// classification's tolerance from `outer`'s boundary
/// ([`crate::query::distance_bound::face_distance_lower_bound`] over `outer`'s faces), or
/// its classification is not decided.
fn point_in(
k: &Kernel,
inner: &mut Brep,
p_inner: &Prepared,
outer: &Brep,
) -> Result<Option<bool>> {
use crate::query::distance_bound::face_distance_lower_bound;
let tol = brep_zone_tol_3d(k, outer, SmallTopoType::NormalSize);
let clear = 2.0 * tol;
for fp in &p_inner.faces {
for piece in &fp.pieces {
let (_, uv) = piece.centre();
let Some(p) = on_face(k, inner, fp, Point2::new(uv[0], uv[1]))? else {
continue;
};
let mut far = true;
for &g in outer.face_list() {
if face_distance_lower_bound(k, outer, g, p, clear)? <= clear {
far = false;
break;
}
}
if !far {
continue;
}
let mut classifier = BrepClassifier::of_brep(outer);
let pc = match classifier.point_3d_classify(k, p, tol, true) {
Ok(pc) => pc,
Err(e) if e.is_cancelled() => return Err(e),
Err(_) => return Ok(None),
};
return Ok(match (pc.point_class(), pc.object()) {
(PointClassType::Region, Some(EntityRef::Region(r))) => {
Some(region_material(classifier.brep(), r)?)
}
_ => None,
});
}
}
Ok(None)
}
/// Whether `a` and `b` meet at `tol` (module docs), `a` prepared once for several `b`s.
///
/// # Errors
///
/// `InvalidInput` for a tolerance not finite and positive, or a body without faces; those
/// of the pieces, the classifications' cancellation and the closest-point solves' setup.
pub fn meets_prepared(
k: &Kernel,
a: &mut Brep,
pa: &Prepared,
b: &mut Brep,
tol: f64,
) -> Result<Contact> {
if !(tol.is_finite() && tol > 0.0) {
return Err(SolidError::invalid_input(
OP,
"the tolerance is finite and positive",
));
}
let pb = Prepared::new(k, b, pa.origin)?;
let stop = |lower: f64, upper: f64| upper <= tol || lower > tol;
let (lower, upper, witness, done) = bracket(k, a, pa, b, &pb, tol, &stop)?;
let verdict = if upper <= tol {
Verdict::Yes
} else if !done {
Verdict::Undecided
} else {
// the boundaries are more than tol apart: they meet only if one encloses the other
match (point_in(k, b, &pb, a)?, point_in(k, a, pa, b)?) {
(Some(true), _) | (_, Some(true)) => Verdict::Yes,
(Some(false), Some(false)) => Verdict::No,
_ => Verdict::Undecided,
}
};
Ok(Contact {
verdict,
lower,
upper,
witness,
})
}
/// [`meets_prepared`] for one pair: `a` prepared about the centre of both bodies' boxes.
///
/// # Errors
///
/// Those of [`meets_prepared`].
pub fn meets(k: &Kernel, a: &mut Brep, b: &mut Brep, tol: f64) -> Result<Contact> {
let origin = common_origin(k, a, b)?;
let pa = Prepared::new(k, a, origin)?;
meets_prepared(k, a, &pa, b, tol)
}
/// The bracket of the distance between `a`'s and `b`'s boundaries, refined until it is at
/// most `width` wide (relative to the upper end, or absolute below 1) or the pairs reach
/// [`MAX_PAIRS`]; the verdict is `Yes` when it is that narrow, `Undecided` when the work
/// ran out first. The bracket always holds the distance; it can stay wider than `width`
/// where a nearest point lies on a face's trim, since a piece straddling the trim carries
/// surface off the face and is cut finer only along the trim.
///
/// # Errors
///
/// `InvalidInput` for a width not finite and positive; those of [`meets`].
pub fn boundary_distance(k: &Kernel, a: &mut Brep, b: &mut Brep, width: f64) -> Result<Contact> {
if !(width.is_finite() && width > 0.0) {
return Err(SolidError::invalid_input(
OP,
"the width is finite and positive",
));
}
let origin = common_origin(k, a, b)?;
let pa = Prepared::new(k, a, origin)?;
let pb = Prepared::new(k, b, origin)?;
let stop = |lower: f64, upper: f64| upper - lower <= width * upper.max(1.0);
let (lower, upper, witness, done) = bracket(k, a, &pa, b, &pb, f64::INFINITY, &stop)?;
Ok(Contact {
verdict: if done {
Verdict::Yes
} else {
Verdict::Undecided
},
lower,
upper,
witness,
})
}
+181 -72
View File
@@ -44,15 +44,18 @@ pub const MAX_PATCHES: usize = 4096;
/// A piece of a face's surface: its homogeneous coordinates about the point, as Bernstein
/// polynomials on the unit square, and the absolute error each coefficient may carry.
#[derive(Debug, Clone)]
struct Piece {
pub(crate) struct Piece {
/// `X W - p W`, `Y W - p W`, `Z W - p W`, `W`.
h: [Bernstein2; 4],
pub(crate) h: [Bernstein2; 4],
/// The bound on every coefficient's error.
err: f64,
pub(crate) err: f64,
/// The certified lower bound of the piece's distance from the point.
lower: f64,
pub(crate) lower: f64,
/// Its subdivision depth.
depth: usize,
pub(crate) depth: usize,
/// Its rectangle in the surface's NURB parameters, `[u0, u1, v0, v1]` (to the rounding
/// of the splits that made it).
pub(crate) uv: [f64; 4],
}
impl PartialEq for Piece {
@@ -74,21 +77,62 @@ impl Ord for Piece {
}
impl Piece {
/// The piece with its bound, the coefficients `h` within `err` each.
fn new(h: [Bernstein2; 4], err: f64, depth: usize) -> Result<Self> {
/// The piece with its bound, the coefficients `h` within `err` each, over `uv`.
fn new(h: [Bernstein2; 4], err: f64, depth: usize, uv: [f64; 4]) -> Result<Self> {
let lower = hull_bound(&h, err)?;
Ok(Self {
h,
err,
lower,
depth,
uv,
})
}
/// The box of its Euclidean control points about the point, each coordinate widened
/// by the error a quotient of coefficients within `err` can carry: every point of the
/// piece lies in it (the hull, with positive weights).
pub(crate) fn widened_box(&self) -> Result<([f64; 3], [f64; 3])> {
let (lo, hi, widen) = euclid_box(&self.h, self.err)?;
Ok((lo.map(|l| l - widen), hi.map(|u| u + widen)))
}
/// Its centre's Euclidean point about the point, and the centre's parameters.
pub(crate) fn centre(&self) -> ([f64; 3], [f64; 2]) {
let [x, y, z, w] = &self.h;
let wc = w.eval(0.5, 0.5);
let [u0, u1, v0, v1] = self.uv;
(
[
x.eval(0.5, 0.5) / wc,
y.eval(0.5, 0.5) / wc,
z.eval(0.5, 0.5) / wc,
],
[0.5 * (u0 + u1), 0.5 * (v0 + v1)],
)
}
/// The certified range of `x . u` over the piece, for a direction `u`: the least and
/// largest of its Euclidean control points' (its hull), each moved out by the widening
/// along `u` and the products' rounding.
pub(crate) fn extent_along(&self, u: [f64; 3]) -> Result<(f64, f64)> {
let (lo, hi, widen) = euclid_box(&self.h, self.err)?;
let size = lo.iter().chain(&hi).fold(0.0_f64, |m, v| m.max(v.abs())) + widen;
let norm1 = u[0].abs() + u[1].abs() + u[2].abs();
let (mut least, mut most) = (f64::INFINITY, f64::NEG_INFINITY);
for e in euclid_points(&self.h) {
let d = e[0] * u[0] + e[1] * u[1] + e[2] * u[2];
least = least.min(d);
most = most.max(d);
}
let slack = widen * norm1 + gamma(3) * size * norm1;
Ok((least - slack, most + slack))
}
/// Its four quarters: one de Casteljau level each way, which adds the rounding of a
/// convex combination per level (`gamma` of twice the degree plus two) of the largest
/// coefficient.
fn quarters(&self) -> Result<[Self; 4]> {
pub(crate) fn quarters(&self) -> Result<[Self; 4]> {
let largest = self
.h
.iter()
@@ -108,14 +152,23 @@ impl Piece {
split(&self.h[2]),
split(&self.h[3]),
];
let [u0, u1, v0, v1] = self.uv;
let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
let rects = [
[u0, um, v0, vm],
[u0, um, vm, v1],
[um, u1, v0, vm],
[um, u1, vm, v1],
];
let mut out = Vec::new();
for i in 0..4 {
for (i, uv) in rects.into_iter().enumerate() {
let pick =
|a: &[Bernstein2; 4]| a.get(i).cloned().ok_or_else(|| SolidError::sm_err(OP));
out.push(Self::new(
[pick(&x)?, pick(&y)?, pick(&z)?, pick(&w)?],
err,
self.depth + 1,
uv,
)?);
}
out.try_into().map_err(|_| SolidError::sm_err(OP))
@@ -130,42 +183,15 @@ impl Piece {
/// least the least P . u, and |X| >= X . u / |u|), which, unlike the box, tightens with the
/// square of the piece's size on a piece tilted to the axes.
fn hull_bound(h: &[Bernstein2; 4], err: f64) -> Result<f64> {
let [x, y, z, w] = h;
let (xs, ys, zs, ws) = (
x.coefficients(),
y.coefficients(),
z.coefficients(),
w.coefficients(),
);
let w_min = ws.iter().copied().fold(f64::INFINITY, f64::min);
if !above(w_min - err, 0.0) {
return Err(SolidError::Unsupported {
operation: OP,
reason: "a patch's weights are not proved positive",
});
}
let mut points = Vec::new();
let mut lo = [f64::INFINITY; 3];
let mut hi = [f64::NEG_INFINITY; 3];
let mut size = 0.0_f64;
let (lo, hi, widen) = euclid_box(h, err)?;
let points = euclid_points(h);
let size = lo.iter().chain(&hi).fold(0.0_f64, |m, v| m.max(v.abs()));
let mut sum_points = [0.0_f64; 3];
for (((&a, &b), &c), &wi) in xs.iter().zip(ys).zip(zs).zip(ws) {
let e = [a / wi, b / wi, c / wi];
for (k, v) in e.into_iter().enumerate() {
size = size.max(v.abs());
if let (Some(l), Some(u), Some(t)) =
(lo.get_mut(k), hi.get_mut(k), sum_points.get_mut(k))
{
*l = l.min(v);
*u = u.max(v);
*t += v;
}
for e in &points {
for (t, v) in sum_points.iter_mut().zip(e) {
*t += v;
}
points.push(e);
}
// |(v + dv) / (w + dw) - v / w| <= err (1 + |v / w|) / (w - err), and the quotient's
// own rounding
let widen = err * (1.0 + size) / (w_min - err) + gamma(2) * size;
let mut sum = 0.0_f64;
for (l, u) in lo.into_iter().zip(hi) {
let out = (l - widen).max(-(u + widen)).max(0.0);
@@ -202,6 +228,61 @@ fn hull_bound(h: &[Bernstein2; 4], err: f64) -> Result<f64> {
Ok(bound.max(0.0))
}
/// The Euclidean control points of the homogeneous coefficients `h`.
fn euclid_points(h: &[Bernstein2; 4]) -> Vec<[f64; 3]> {
let [x, y, z, w] = h;
x.coefficients()
.iter()
.zip(y.coefficients())
.zip(z.coefficients())
.zip(w.coefficients())
.map(|(((&a, &b), &c), &wi)| [a / wi, b / wi, c / wi])
.collect()
}
/// The box of the Euclidean control points of `h`, and how far each coordinate is to be
/// widened for coefficients within `err` (and the quotients' own rounding): `(lo, hi,
/// widen)`.
fn euclid_box(h: &[Bernstein2; 4], err: f64) -> Result<([f64; 3], [f64; 3], f64)> {
let [x, y, z, w] = h;
let (xs, ys, zs, ws) = (
x.coefficients(),
y.coefficients(),
z.coefficients(),
w.coefficients(),
);
let w_min = ws.iter().copied().fold(f64::INFINITY, f64::min);
if !above(w_min - err, 0.0) {
return Err(SolidError::Unsupported {
operation: OP,
reason: "a patch's weights are not proved positive",
});
}
let mut lo = [f64::INFINITY; 3];
let mut hi = [f64::NEG_INFINITY; 3];
let mut size = 0.0_f64;
for (((&a, &b), &c), &wi) in xs.iter().zip(ys).zip(zs).zip(ws) {
let e = [a / wi, b / wi, c / wi];
for (k, v) in e.into_iter().enumerate() {
size = size.max(v.abs());
if let (Some(l), Some(u)) = (lo.get_mut(k), hi.get_mut(k)) {
*l = l.min(v);
*u = u.max(v);
}
}
}
// |(v + dv) / (w + dw) - v / w| <= err (1 + |v / w|) / (w - err), and the quotient's
// own rounding
let widen = err * (1.0 + size) / (w_min - err) + gamma(2) * size;
if !(widen.is_finite() && lo.iter().chain(&hi).all(|v| v.is_finite())) {
return Err(SolidError::Unsupported {
operation: OP,
reason: "a patch's box is not finite",
});
}
Ok((lo, hi, widen))
}
/// A certified lower bound of the distance from `p` to face `f` of `b`: no point of the face
/// is nearer to `p`. The face is bounded by its surface over the face's UV box (its trims
/// lie inside it): [`surface_distance_lower_bound`] on them.
@@ -320,6 +401,51 @@ pub fn surface_distance_lower_bound(
"the bound asked for is finite and not negative",
));
}
let (pieces, slack) = pieces_about(k, surf, uv_box, p)?;
let want_net = want + slack;
let mut made = pieces.len();
let mut heap: BinaryHeap<Piece> = pieces.into_iter().collect();
let off = |lower: f64| (lower - slack).max(0.0);
loop {
// (the box is inside the patches' range, so some patch overlaps it)
let nearest = heap.pop().ok_or_else(|| SolidError::sm_err(OP))?;
if nearest.lower >= want_net || made >= MAX_PATCHES {
return Ok(off(nearest.lower));
}
// the piece's centre is a point of the net over the box: nearer than the target, no
// bound reaches it
let (centre, _) = nearest.centre();
if (centre[0] * centre[0] + centre[1] * centre[1] + centre[2] * centre[2]).sqrt() < want_net
{
return Ok(off(nearest.lower));
}
for q in nearest.quarters()? {
heap.push(q);
}
made += 4;
}
}
/// The pieces of `surf` over `uv_box` (as [`surface_distance_lower_bound`] takes it) about
/// `p`: its NURBS form's Bézier patches restricted to the box, translated by `-p`, each
/// with its coefficients' error (the form's against its definition, the extraction's and
/// the translation's, module docs); and the slack every distance from them is to lose (a
/// net's certified bound from its definition, 0 for a surface that evaluates on its net).
/// The one construction of the face pieces that [`surface_distance_lower_bound`] and the
/// contact test between bodies ([`crate::query::contact`]) bound distances with.
///
/// # Errors
///
/// As [`surface_distance_lower_bound`]'s, but for `want`.
pub(crate) fn pieces_about(
k: &Kernel,
surf: &Surface,
uv_box: Option<&Extent2d>,
p: Point3,
) -> Result<(Vec<Piece>, f64)> {
if !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite()) {
return Err(SolidError::invalid_input(OP, "point not finite"));
}
if !measurable(k, surf)? {
return Err(SolidError::Unsupported {
operation: OP,
@@ -424,9 +550,7 @@ pub fn surface_distance_lower_bound(
));
}
}
let want_net = want + slack;
let mut heap = BinaryHeap::new();
let mut made = 0_usize;
let mut pieces = Vec::new();
for patch in patches.iter().flatten() {
let (pu, pv) = patch.degrees();
let (uk, vk) = (patch.u_knots(), patch.v_knots());
@@ -496,33 +620,15 @@ pub fn surface_distance_lower_bound(
} else {
h
};
heap.push(Piece::new(h, err, 0)?);
made += 1;
}
let off = |lower: f64| (lower - slack).max(0.0);
loop {
// (the box is inside the patches' range, so some patch overlaps it)
let nearest = heap.pop().ok_or_else(|| SolidError::sm_err(OP))?;
if nearest.lower >= want_net || made >= MAX_PATCHES {
return Ok(off(nearest.lower));
}
// the piece's centre is a point of the net over the box: nearer than the target, no
// bound reaches it
let [x, y, z, w] = &nearest.h;
let wc = w.eval(0.5, 0.5);
let centre = (
x.eval(0.5, 0.5) / wc,
y.eval(0.5, 0.5) / wc,
z.eval(0.5, 0.5) / wc,
);
if (centre.0 * centre.0 + centre.1 * centre.1 + centre.2 * centre.2).sqrt() < want_net {
return Ok(off(nearest.lower));
}
for q in nearest.quarters()? {
heap.push(q);
}
made += 4;
let uv = [
u0 + a0 * (u1 - u0),
u0 + a1 * (u1 - u0),
v0 + b0 * (v1 - v0),
v0 + b1 * (v1 - v0),
];
pieces.push(Piece::new(h, err, 0, uv)?);
}
Ok((pieces, slack))
}
#[cfg(test)]
@@ -890,7 +996,10 @@ mod tests {
.unwrap()
});
let exact = h.each_ref().map(exact_quarters);
let quarters = Piece::new(h, 0.0, 0).unwrap().quarters().unwrap();
let quarters = Piece::new(h, 0.0, 0, [0.0, 1.0, 0.0, 1.0])
.unwrap()
.quarters()
.unwrap();
for (q, quarter) in quarters.iter().enumerate() {
for (axis, component) in quarter.h.iter().enumerate() {
for (rounded, reference) in
+334 -108
View File
@@ -11,7 +11,6 @@
use crate::error::{Result, SolidError};
use crate::geom::axis2_placement::Axis2Placement;
use crate::geom::extent2d::Extent2d;
use crate::geom::vector2d::Point2;
use crate::kernel::Kernel;
use crate::math::{Vec2, Vec3, PI};
use crate::nurbs::b_spline_curve::BSplineCurve;
@@ -69,42 +68,6 @@ pub enum Implicit {
},
}
impl Implicit {
/// How far `x` is from this surface.
#[must_use]
pub fn distance(&self, x: Vec3) -> f64 {
// the axial and radial parts of `x` about an axis through `o`
let about = |o: Vec3, axis: Vec3| {
let v = x - o;
let h = v.dot(axis);
(h, (v - axis * h).length())
};
match *self {
Self::Plane { origin, normal } => (x - origin).dot(normal).abs(),
Self::Cone {
origin,
axis,
radius,
tan_angle,
} => {
let (h, rho) = about(origin, axis);
(rho - (radius + tan_angle * h)).abs() / (1.0 + tan_angle * tan_angle).sqrt()
}
Self::Sphere { centre, radius } => ((x - centre).length() - radius).abs(),
Self::Torus {
centre,
axis,
major,
minor,
} => {
let (h, rho) = about(centre, axis);
let (a, b) = (rho - major, h);
((a * a + b * b).sqrt() - minor).abs()
}
}
}
}
/// A polynomial in Bernstein form over a patch, degrees `(p, q)`, `c[i (q + 1) + j]`.
#[derive(Debug, Clone, PartialEq)]
struct BPoly {
@@ -343,7 +306,7 @@ fn bezier_on<'a>(
f.c.iter().all(within)
}
pub(crate) const OP_EXACT: &str = "surface::implicit::check_exact_net";
const OP_EXACT: &str = "surface::implicit::check_exact_net";
/// A quadric `x.A.x + 2 b.x + c = 0` as `(A, b, c)`.
pub type QuadricEquation = ([[f64; 3]; 3], [f64; 3], f64);
@@ -439,46 +402,6 @@ pub fn check_exact_net(k: &Kernel, net: &Surface, check: &ExactNetCheck) -> Resu
Ok(())
}
/// The number of intervals [`net_miss`] samples a net at, each way.
const MISS_SAMPLES: u32 = 16;
/// How far `net` strays from the class's `surface`, sampled over its domain. A guard, not a
/// certificate: [`check_exact_net`] is the proof, and a net that passes only this keeps no
/// definition. It tells a net that the class's rounding leaves off the surface from one
/// built wrongly.
///
/// # Errors
///
/// Those of the evaluations.
pub fn net_miss(k: &Kernel, net: &Surface, surface: &ClassSurface) -> Result<f64> {
let dom = net.natural_uv_domain_with_nurb(k)?;
let (lo, hi) = (dom.min(), dom.max());
let mut worst = 0.0_f64;
for i in 0..=MISS_SAMPLES {
for j in 0..=MISS_SAMPLES {
let (fu, fv) = (
f64::from(i) / f64::from(MISS_SAMPLES),
f64::from(j) / f64::from(MISS_SAMPLES),
);
let uv = Point2::new(lo.x + (hi.x - lo.x) * fu, lo.y + (hi.y - lo.y) * fv);
let x = net.evaluate_point(k, uv)?;
let miss = match surface {
ClassSurface::Implicit(a) => a.distance(x),
ClassSurface::Canal {
spine, map, radius, ..
} => {
let w = if map.along_v { uv.y } else { uv.x };
let ivl = spine.natural_interval();
let s = (map.a + map.b * w).max(ivl.min()).min(ivl.max());
((x - spine.evaluate_point(s)?).length() - *radius).abs()
}
};
worst = worst.max(miss);
}
}
Ok(worst)
}
/// A Bezier segment of a rational curve, Bernstein polynomials of degree `(p, 0)`: the
/// homogeneous `(Y - O V)` components and the weight `V`.
type CurveSegment = ([BPoly; 3], BPoly);
@@ -1200,12 +1123,37 @@ impl AnalyticSupport {
// a surface of revolution: the one the kernel recognises in the form, which must be
// this one, take the form as its NURB and evaluate as the form at the form's own
// parameters
let not_form = || {
SolidError::invalid_input(
OP_BUILD,
"a support's analytic surface is not its NURBS form's parametrisation",
)
};
let surface = self.on_form(k, &form)?;
if !evaluates_as(k, &surface, &form)? {
return Err(not_form());
}
Ok(surface)
}
/// The largest ratio, over the form's Greville points, of this support's analytic
/// surface's distance from `form` to the rounding [`evaluates_as`] derives there (#3185):
/// at most 1 where [`Self::build`]'s parametrisation test passes. The surface is the one
/// `build` makes (the kernel's recognition of `form`, carrying this support's numbers);
/// `lies_on` and the placement's axes are not checked here. `None` for a plane, or a
/// surface the bound does not cover.
///
/// # Errors
///
/// `InvalidInput` where the kernel recognises no surface of this kind in `form`; those of
/// the recognisers, the conversions and the evaluations.
pub fn parametrisation_margin(&self, k: &Kernel, form: &BSplineSurface) -> Result<Option<f64>> {
if matches!(self, Self::Plane { .. }) {
return Ok(None);
}
let surface = self.on_form(k, form)?;
evaluation_margin(k, &surface, form)
}
/// The surface of revolution of this support's kind the kernel recognises in `form`,
/// carrying this support's own numbers and `form` as its NURB ([`Self::build`] after its
/// checks of the point set, before the parametrisation's).
fn on_form(&self, k: &Kernel, form: &BSplineSurface) -> Result<Surface> {
let form = form.copy();
let mut surface = recognise(k, self.kind(), &form)?.ok_or_else(not_form)?;
// the recognised surface must be of this support's kind; the numbers recognition
// recomputed are not compared with the support's, because it recomputes them from
@@ -1255,13 +1203,18 @@ impl AnalyticSupport {
}
_ => return Err(not_form()),
}
if !evaluates_as(k, &surface, &form)? {
return Err(not_form());
}
Ok(surface)
}
}
/// [`AnalyticSupport::build`]'s refusal of a surface that is not its form's parametrisation.
fn not_form() -> SolidError {
SolidError::invalid_input(
OP_BUILD,
"a support's analytic surface is not its NURBS form's parametrisation",
)
}
/// A cylinder's or cone's recognised parametrisation carried by the support's own
/// `placement`: its heights are measured along the axis from the placement's origin, so the
/// recognised heights move by how far along the axis the recognised origin lies from it.
@@ -1336,41 +1289,314 @@ fn recognise(k: &Kernel, kind: SupportKind, form: &BSplineSurface) -> Result<Opt
}
/// Whether `surface` evaluates as `form` at the form's Greville parameters (each control
/// point's), within the rounding of the two evaluations: `gamma_K` times the form's size,
/// `K = 16 (p + q + 2)` for the rational evaluation of degrees `(p, q)` and the analytic
/// one's conversion of the parameters and its trigonometry, a few ulps per operation.
/// point's): at each, the analytic surface's point and the form's agree within the bound
/// [`Rounding::at`] derives for that point, term by term below. The analytic side is the
/// **STEP** evaluation of the converted parameters ([`Surface::convert_uv_from_nurbs_to_step`]
/// and [`Surface::evaluate_step_point`]), which a cone, sphere, torus and surface of
/// revolution compute from their placement, radii, semi-angle and generator.
/// [`Surface::evaluate_point`] would not do: a sphere, a torus and a surface of revolution
/// take it from their NURBS base, and that base is the `form` the caller has just installed,
/// so it would compare the form with itself and read nothing of what the caller imposed
/// (#2588).
///
/// The analytic side is the **STEP** evaluation of the converted parameters
/// ([`Surface::convert_uv_from_nurbs_to_step`] and [`Surface::evaluate_step_point`]),
/// which a cone, sphere, torus and surface of revolution compute from their placement,
/// radii, semi-angle and generator. [`Surface::evaluate_point`] would not do: a sphere, a
/// torus and a surface of revolution take it from their NURBS base, and that base is the
/// `form` the caller has just installed, so it would compare the form with itself and read
/// nothing of what the caller imposed (#2588; the cone already converted and evaluated
/// analytically, which is why only the other kinds went unchecked).
/// What the bound forgives and what it tests (#3185): it forgives the rounding of both
/// evaluations and of the two parameter conversions, nothing else. It does not forgive the
/// label's frame turned about the axis against the converter's, its axis, origin or radii,
/// a form whose parameter is not the analytic surface's (a reparametrised arc), nor the
/// form's distance from the analytic surface: those are what the comparison proves.
///
/// `u` is the unit roundoff, `gamma_n = n u / (1 - n u)`. Stated assumption: the libm
/// `sin`, `cos`, `tan` and `atan2` are within one ulp (relative `2 u`).
///
/// 1. **The form** (`Surface::eval`, Cox-de Boor then two blends): each basis value is a
/// nonnegative combination over `p` levels of at most 6 operations (two knot differences,
/// a sum, a division, a product and a sum), so relative `gamma_6p`; the blends sum
/// `q + 1` then `p + 1` products. With positive weights the homogeneous point's error is
/// at most `gamma_(7 (p + q) + 4) W size` and the weight's `gamma_(7 (p + q) + 4) W`, so
/// the Euclidean point's, after the division, `E_form = (2 gamma_(7 (p + q) + 4) + u)
/// size` (`gamma_(7 (p + q) + 4) size` when polynomial), `size` the net's largest point.
/// 2. **The STEP point**, `O + m (cos u X + sin u Y) + h Z`, `m` and `h` the generator's
/// radial and axial parts (the torus `R + r cos v`, `r sin v`; the sphere `r cos v`,
/// `r sin v`; the cone `r0 + v tan a`, `v`). An angle `x` in degrees goes to radians in
/// two operations of the rounded `PI`: `gamma_3 |x|`; with one ulp of the cosine and sine,
/// each is off by `dc(x) = 2 u + gamma_3 |x|`. Per coordinate (unit axes) the error is at
/// most `gamma_3 (|O| + M + H) + M (gamma_2 + 2 dc(u)) + dm + dh`, `M`, `H` the sizes of
/// `m`, `h` and `dm`, `dh` their errors: the torus `gamma_3 (R + r) + 2 r dc(v)`, the
/// sphere `gamma_1 r + 2 r dc(v)`, the cone `gamma_2 (|r0| + |v t|) + |v| dt` (`dt =
/// (1 + t^2) gamma_3 |a| + 2 u |t|` for the tangent of the half angle `a`); a general
/// surface of revolution rotates its generator's point `G` (a B-spline: item 1's bound of
/// its own net, `E_gen`) about the axis, `gamma_8 (|O| + 3 D) + 2 D dc(u) + E_gen`, `D`
/// the net's reach from `O`. `E_step` is `sqrt 3` times the coordinate bound.
/// 3. **The generator's parameter**: a circle (torus, sphere) inverts its NURBS point by
/// `atan2` about its centre `c`: at its radius `r` the error is that point's (`E_form`)
/// plus `gamma_4 (size + |c|)` and `gamma_4 r |v|` of the angle's own rounding; a line
/// (cone) maps it affinely in 6 operations, `gamma_6 2 (|v0| + |v1|)` along the axis and
/// its slope, times `1 + |t|`; a B-spline generator's parameter is its NURBS's (none).
/// 4. **The sweep's parameter**: the polar converter evaluates its circle's NURBS at the
/// parameter (item 1's bound of that NURBS's own net, `E_polar`) and inverts the point by
/// `atan2` about the circle's centre `c` in its frame: at the circle's radius `rc` the
/// angle is off by `(E_polar + gamma_4 (rc + 2 |c|)) / rc`, plus `gamma_4 |s|` of the
/// angle's own rounding and the degrees' period (`gamma_1 2 pi`), times the STEP point's
/// distance from that axis (the lever). The converter's ends are its domain's, which
/// recognition takes from the form's own end meridians (#3199), so they are the inverted
/// ends to the same rounding.
///
/// A surface of another kind, or a general surface of revolution whose generator is not a
/// B-spline, has no bound here and is not proved.
///
/// # Errors
///
/// Those of the parameter conversion and the evaluations and of the form's knots.
fn evaluates_as(k: &Kernel, surface: &Surface, form: &BSplineSurface) -> Result<bool> {
Ok(evaluation_margin(k, surface, form)?.is_some_and(|m| m <= 1.0))
}
/// [`evaluates_as`]'s largest ratio of distance to bound over the Greville points (infinite
/// for a distance that is not a number); `None` for a surface the bound does not cover.
fn evaluation_margin(k: &Kernel, surface: &Surface, form: &BSplineSurface) -> Result<Option<f64>> {
let (net, p, q) = euclidean_net(form)?;
let size = net.iter().flatten().fold(0.0_f64, |a, x| a.max(x.length()));
let tol = gamma(16 * (p + q + 2)) * size;
let Some(r) = Rounding::of(surface, form.is_rational(), p, q, size, &net)? else {
return Ok(None);
};
let mut worst = 0.0_f64;
for u in greville(form, true)? {
for v in greville(form, false)? {
let uv = Vec2::new(u, v);
let step_uv = surface.convert_uv_from_nurbs_to_step(k, uv)?;
let off =
(surface.evaluate_step_point(k, step_uv)? - form.evaluate_point(uv)?).length();
if !matches!(
off.partial_cmp(&tol),
Some(core::cmp::Ordering::Less | core::cmp::Ordering::Equal)
) {
return Ok(false);
}
let s = surface.evaluate_step_point(k, step_uv)?;
let f = form.evaluate_point(uv)?;
let ratio = (s - f).length() / r.at(step_uv, s, f);
worst = if ratio.is_nan() {
f64::INFINITY
} else {
worst.max(ratio)
};
}
}
Ok(true)
Ok(Some(worst))
}
/// A surface of revolution's generator, for [`Rounding`]'s item 2 and 3.
#[derive(Debug, Clone, Copy)]
enum Generator {
/// `R + r cos v`, `r sin v` about the circle of centre `c`.
Torus { major: f64, minor: f64, c: f64 },
/// `r cos v`, `r sin v` about the circle of centre `c`.
Sphere { radius: f64, c: f64 },
/// `r0 + v t`, `v` (the half angle `a` in radians), its STEP `v` over `[v0, v1]`.
Cone {
r0: f64,
t: f64,
a: f64,
v0: f64,
v1: f64,
},
/// A B-spline generator, its own evaluation's bound.
Spline { e_gen: f64 },
}
/// The pieces of [`evaluates_as`]'s bound that do not depend on the point.
#[derive(Debug, Clone, Copy)]
struct Rounding {
e_form: f64,
origin: Vec3,
/// The net's reach from the origin.
reach: f64,
generator: Generator,
/// The sweep converter's circle: centre, axes.
polar: (Vec3, Vec3, Vec3, f64, f64),
}
/// `|v|`'s largest coordinate.
fn max_coord(v: Vec3) -> f64 {
v.x.abs().max(v.y.abs()).max(v.z.abs())
}
/// A rational (or polynomial) evaluation's bound of degrees summing to `n` over points of
/// size `size` ([`evaluates_as`] item 1).
fn eval_bound(rational: bool, n: usize, size: f64) -> f64 {
let g = gamma(7 * n + 4);
if rational {
(2.0 * g + crate::nurbs::math::UNIT) * size
} else {
g * size
}
}
impl Rounding {
/// The bound's data for `surface` (a cone, sphere, torus or surface of revolution with a
/// B-spline generator) on its form of degrees `(p, q)`, net `net`; `None` for any other.
fn of(
surface: &Surface,
rational: bool,
p: usize,
q: usize,
size: f64,
net: &[Vec<Vec3>],
) -> Result<Option<Self>> {
use crate::curve::curve::CurveKind;
let e_form = eval_bound(rational, p + q, size);
let circle_centre = |rev: &crate::surface::surf_of_revolution::SurfOfRevolution| match rev
.gen_curve
.as_ref()
.map(crate::curve::curve::Curve::kind)
{
Some(CurveKind::Ellipse(e)) => Some(e.position.origin().length()),
_ => None,
};
let (rev, generator) = match surface.kind() {
SurfaceKind::Torus(t) => {
let Some(c) = circle_centre(&t.base) else {
return Ok(None);
};
(
&t.base,
Generator::Torus {
major: t.major_radius,
minor: t.minor_radius,
c,
},
)
}
SurfaceKind::Sphere(sp) => {
let Some(c) = circle_centre(&sp.base) else {
return Ok(None);
};
(
&sp.base,
Generator::Sphere {
radius: sp.radius,
c,
},
)
}
SurfaceKind::Cone(c) => {
let a = c.semi_angle_deg * PI / 180.0;
let d = c.rev.anal_uv_domain;
(
&c.rev,
Generator::Cone {
r0: c.base_radius,
t: c.tan_semi_angle(),
a,
v0: d.v_min(),
v1: d.v_max(),
},
)
}
SurfaceKind::SurfOfRevolution(rev) => {
let Some(CurveKind::BSpline(g)) =
rev.gen_curve.as_ref().map(crate::curve::curve::Curve::kind)
else {
return Ok(None);
};
let pts = g.control_points()?;
let gsize = pts.iter().fold(0.0_f64, |m, c| {
let w = if c.w == NOW { 1.0 } else { c.w };
m.max((Vec3::new(c.x, c.y, c.z) * (1.0 / w)).length())
});
let rational = pts.iter().any(|c| c.w != NOW);
(
&**rev,
Generator::Spline {
e_gen: eval_bound(rational, g.degree()?, gsize),
},
)
}
_ => return Ok(None),
};
let Some(pc) = rev.polar_curve() else {
return Ok(None);
};
let e_polar = {
let pts = pc.base().control_points()?;
let psize = pts.iter().fold(0.0_f64, |m, c| {
let w = if c.w == NOW { 1.0 } else { c.w };
m.max((Vec3::new(c.x, c.y, c.z) * (1.0 / w)).length())
});
eval_bound(pts.iter().any(|c| c.w != NOW), pc.base().degree()?, psize)
};
let origin = rev.position.origin();
let reach = net
.iter()
.flatten()
.fold(0.0_f64, |m, x| m.max((*x - origin).length()));
Ok(Some(Self {
e_form,
origin,
reach,
generator,
polar: (
pc.position.origin(),
pc.position.x_axis(),
pc.position.y_axis(),
pc.radius_x,
e_polar,
),
}))
}
/// The bound at the STEP parameters `step` (degrees, and the generator's), the STEP
/// point `s` and the form's point `f` ([`evaluates_as`] items 1 to 4).
fn at(&self, step: Vec2, s: Vec3, f: Vec3) -> f64 {
let u = crate::nurbs::math::UNIT;
let rad = |deg: f64| deg * PI / 180.0;
let dc = |deg: f64| 2.0 * u + gamma(3) * rad(deg).abs();
let o = max_coord(self.origin);
let size_f = f.length();
// item 2 (per coordinate) and item 3 (as a length)
let (coord, generator) = match self.generator {
Generator::Torus { major, minor, c } => {
let (m, h) = (major + minor, minor);
let dm = gamma(3) * (major + minor) + 2.0 * minor * dc(step.y);
let dh = gamma(1) * minor + 2.0 * minor * dc(step.y);
(
gamma(3) * (o + m + h) + m * (gamma(2) + 2.0 * dc(step.x)) + dm + dh,
self.e_form + gamma(4) * (size_f + c) + gamma(4) * minor * rad(step.y).abs(),
)
}
Generator::Sphere { radius, c } => {
let dm = gamma(1) * radius + 2.0 * radius * dc(step.y);
(
gamma(3) * (o + 2.0 * radius)
+ radius * (gamma(2) + 2.0 * dc(step.x))
+ 2.0 * dm,
self.e_form + gamma(4) * (size_f + c) + gamma(4) * radius * rad(step.y).abs(),
)
}
Generator::Cone { r0, t, a, v0, v1 } => {
let v = step.y;
let m = r0.abs() + (v * t).abs();
let dt = (1.0 + t * t) * gamma(3) * a.abs() + 2.0 * u * t.abs();
let dm = gamma(2) * m + v.abs() * dt;
(
gamma(3) * (o + m + v.abs()) + m * (gamma(2) + 2.0 * dc(step.x)) + dm,
gamma(6) * 2.0 * (v0.abs() + v1.abs()) * (1.0 + t.abs()),
)
}
Generator::Spline { e_gen } => (
gamma(8) * (o + 3.0 * self.reach) + 2.0 * self.reach * dc(step.x) + e_gen,
0.0,
),
};
let e_step = 3.0_f64.sqrt() * coord;
// item 4: the polar converter's inversion of its own circle's point
let (c, x, y, rc, e_polar) = self.polar;
let z = x.cross(y);
let along = |v: Vec3| v - z * v.dot(z);
let lever = along(s - c).length();
let sweep = if rc > 0.0 {
lever
* ((e_polar + gamma(4) * (rc + 2.0 * c.length())) / rc
+ gamma(4) * rad(step.x).abs()
+ gamma(1) * 2.0 * PI)
} else {
// a circle of no radius has no angle: any, at the STEP point's lever
lever * PI
};
self.e_form + e_step + generator + sweep
}
}
const OP_DERIVE: &str = "surface::implicit::AnalyticSupport::derive";
@@ -39,11 +39,62 @@ use crate::geom::vector3d::{Point3, Vec3};
use crate::kernel::Kernel;
use crate::math::EFF_ZERO;
use crate::nurbs::b_spline_curve::BSplineCurve;
use crate::surface::implicit::{check_exact_net, net_miss, ClassSurface, ExactNetCheck, LeaderMap};
use crate::surface::implicit::{
check_exact_net, lies_on, AnalyticSupport, ClassSurface, ExactNetCheck, Implicit, LeaderMap,
};
use crate::surface::procedural_blend::chamfer::ChamferDefinition;
use crate::surface::surface::Surface;
const OP_TRANSFORM: &str = "surface::procedural_blend::exact::ExactRound::transform";
const OP_CIRCLE: &str = "surface::procedural_blend::exact::ExactRound::supports_distance";
/// The torus a round of the circle class is: a surface of revolution's, about its
/// placement's axis, of the circle its generating arc is on (the centre of a rational quadratic
/// arc from its first span's control points: the midpoint of the chord pushed away from the
/// middle point by `a^2 / h`, `a` the half chord, `h` the middle point's distance from the
/// chord's midpoint); or a torus label's own. `None` for any other surface.
///
/// # Errors
///
/// Those of reading the arc's control polygon.
fn own_torus(net: &Surface) -> Result<Option<Implicit>> {
use crate::curve::curve::{Curve, CurveKind};
use crate::surface::surface::SurfaceKind;
match net.kind() {
SurfaceKind::Torus(_) => Ok(AnalyticSupport::of(net)
.as_ref()
.map(AnalyticSupport::implicit)),
SurfaceKind::SurfOfRevolution(rev) => {
let Some(CurveKind::BSpline(arc)) = rev.gen_curve().map(Curve::kind) else {
return Ok(None);
};
let (points, _) = arc.control_polygon()?;
let [p0, p1, p2, ..] = points.as_slice() else {
return Ok(None);
};
let mid = (*p0 + *p2) * 0.5;
let (a, h) = ((*p0 - mid).length(), (mid - *p1).length());
if h.partial_cmp(&0.0) != Some(core::cmp::Ordering::Greater) {
return Ok(None);
}
let centre = mid + (mid - *p1) * (a * a / (h * h));
let position = rev.position();
let (o, axis) = (
position.origin(),
position.x_axis().cross(position.y_axis()),
);
let along = (centre - o).dot(axis);
let across = (centre - o) - axis * along;
Ok(Some(Implicit::Torus {
centre: o + axis * along,
axis,
major: across.length(),
minor: (*p0 - centre).length(),
}))
}
_ => Ok(None),
}
}
/// The exact class of a round whose net is its definition.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -263,7 +314,8 @@ impl ExactRound {
/// Re-certifies `net` against this definition: the supports classified again are of
/// this class, and the net lies on the class's surface derived from them
/// ([`check_exact_net`]). Returns the distance proved, 0.
/// ([`check_exact_net`]). A round of the circle class is certified on its own torus
/// instead ([`ExactRound::supports_distance`]). Returns the distance proved, 0.
///
/// # Errors
///
@@ -271,6 +323,10 @@ impl ExactRound {
/// surface from them, a net off it; those of reading the supports' quadrics and the
/// net's NURBS form.
pub fn recertify(&self, k: &Kernel, net: &Surface) -> Result<f64> {
if self.class == ExactClass::Circle {
self.supports_distance(k, net)?;
return Ok(0.0);
}
let [a, b] = &self.supports;
let blend = classify_supports(k, [a, b], &self.inputs)?;
// the chord (#2486): a line spine between planes, of sizes that give its offsets
@@ -302,19 +358,72 @@ impl ExactRound {
Ok(0.0)
}
/// How far `net` strays from the surface of the class its supports give
/// ([`net_miss`]); `None` where they give it none.
/// A round of the circle class: the torus `net` is ([`own_torus`]: its revolution's
/// axis and generating circle, or its torus label), proved to hold its form to rounding
/// ([`lies_on`]),
/// and how far that torus is from the class's torus derived from the supports
/// ([`class_surface`]): returned, refused past the tolerance the class was read within.
///
/// The class is read within the body's tolerance. Supports only that close to it (a plane
/// 1e-13 off square to a cylinder's axis, read from a record) have no torus touching both
/// exactly, so the round the solver builds, tangent to the supports as they are, is on a
/// torus of its own. Its definition is that torus, at bound 0, a stated distance from the
/// supports' class.
///
/// # Errors
///
/// Those of reading the supports' quadrics and of the evaluations.
pub fn miss(&self, k: &Kernel, net: &Surface) -> Result<Option<f64>> {
/// `InvalidInput` for supports of another class or with no torus, a net of no torus, a
/// net off its torus, or a torus further from the class's than the tolerance; those of
/// reading the supports' quadrics, the net's NURBS form and [`lies_on`].
pub fn supports_distance(&self, k: &Kernel, net: &Surface) -> Result<f64> {
let refused = |why| SolidError::invalid_input(OP_CIRCLE, why);
let [a, b] = &self.supports;
let blend = classify_supports(k, [a, b], &self.inputs)?;
match class_surface(&blend, self.leader.as_ref(), &self.map) {
Some(surface) => net_miss(k, net, &surface).map(Some),
None => Ok(None),
if ExactClass::of_spine(blend.spine) != Some(ExactClass::Circle) {
return Err(refused(
"the supports classified again are not of the class the definition names",
));
}
let Some(ClassSurface::Implicit(Implicit::Torus {
centre,
axis,
major,
minor,
})) = class_surface(&blend, self.leader.as_ref(), &self.map)
else {
return Err(refused("the supports give the class no torus"));
};
let form = crate::blend::fillet_section::station::as_b_spline(k, net)?
.ok_or_else(|| refused("the net has no NURBS form to check"))?;
let own = own_torus(net)?.ok_or_else(|| refused("the net is no torus"))?;
if !lies_on(&form, &own)? {
return Err(refused("the net is off its own torus"));
}
let Implicit::Torus {
centre: c,
axis: d,
major: big_r,
minor: r,
} = own
else {
return Err(refused("the net's own surface is no torus"));
};
// every point of the net's torus moves to the class's torus by at most: the turn of
// its axis (an angle, over the torus's reach from its centre), the move of its
// centre, and the change of its radii
let turn = d.cross(axis).length().atan2(d.dot(axis).abs());
let distance =
turn * (big_r + r) + (c - centre).length() + (big_r - major).abs() + (r - minor).abs();
let within = matches!(
distance.partial_cmp(&self.inputs.tol),
Some(core::cmp::Ordering::Less | core::cmp::Ordering::Equal)
);
if !within {
return Err(refused(
"the net's torus is further from the class's than the tolerance",
));
}
Ok(distance)
}
/// Moves the definition with its net (`Transform`): the supports, the leader and the
+99 -6
View File
@@ -1061,7 +1061,6 @@ impl Torus {
let ref_frame = ref_iso.reference_frame.ok_or_else(missing)?;
let other_frame = other_iso.reference_frame.ok_or_else(missing)?;
let min_rad = other_iso.radius.ok_or_else(missing)?;
let sweep_ivl = ref_iso.angles.ok_or_else(missing)?;
let common_point = other_iso.common_line_point.ok_or_else(missing)?;
let common_vec = other_iso.common_line_vec.ok_or_else(missing)?;
@@ -1069,6 +1068,17 @@ impl Torus {
let torus_origin =
common_point + common_vec.dot(other_frame.origin() - common_point) * common_vec;
// not upstream (#3199): the x axis and the sweep interval from the form's own first
// sweep row's ends about the axis (`sweep_frame`), not from `is_arc`'s fit of it
let Some((torus_x_axis, torus_y_axis, sweep_ivl)) = sweep_frame(
torus_origin,
ref_frame.z_axis(),
fitted_sides(&ref_frame, ref_iso.angles.ok_or_else(missing)?),
meridians(test, swap_uv)?,
) else {
return Ok(None);
};
// get torus orientation and size
// not upstream (WR-501, #1470, opt-in; tangency-labs/smlib@6136a04): the major
// radius runs from the torus origin, not from the first sweep circle's centre,
@@ -1079,9 +1089,7 @@ impl Torus {
other_frame.origin() - ref_frame.origin()
};
let maj_rad = major_vec.length();
let torus_x_axis = ref_frame.x_axis(); // the frame where all circles are parallel
let torus_y_axis = ref_frame.y_axis();
let torus_z_axis = ref_frame.z_axis();
let torus_z_axis = ref_frame.z_axis(); // the frame where all circles are parallel
// torus must have positive radii
if min_rad <= EFF_ZERO || maj_rad <= EFF_ZERO {
@@ -1294,7 +1302,6 @@ impl Torus {
let mut ref_frame = ref_iso.reference_frame.ok_or_else(missing)?;
let other_frame = other_iso.reference_frame.ok_or_else(missing)?;
let min_rad = other_iso.radius.ok_or_else(missing)?;
let sweep_ivl = ref_iso.angles.ok_or_else(missing)?;
let common_point = other_iso.common_line_point.ok_or_else(missing)?;
let common_vec = other_iso.common_line_vec.ok_or_else(missing)?;
@@ -1302,8 +1309,18 @@ impl Torus {
let torus_origin =
common_point + common_vec.dot(other_frame.origin() - common_point) * common_vec;
// not upstream (#3199): the x axis and the sweep interval from the form's own first
// sweep row's ends about the axis (`sweep_frame`), not from `is_arc`'s fit of it
let (x_axis, y_axis, sweep_ivl) = sweep_frame(
torus_origin,
ref_frame.z_axis(),
fitted_sides(&ref_frame, ref_iso.angles.ok_or_else(missing)?),
meridians(srf, swap_uv)?,
)
.ok_or_else(|| SolidError::sm_err(OP_REBUILD))?;
// Install the origin into pRefFrame. // [B328]
let _ = ref_frame.set_canonical(torus_origin, ref_frame.x_axis(), ref_frame.y_axis());
let _ = ref_frame.set_canonical(torus_origin, x_axis, y_axis);
// get torus orientation and size
let major_vec = other_frame.origin() - ref_frame.origin();
@@ -1587,6 +1604,82 @@ impl Revolved for Torus {
/// `SmTorus::CreateIsoParametricCurve` of the surface of revolution `rev` of a torus
/// of `shape` (also called from the base class's rebuilds, where it is virtual).
/// The control points, Euclidean, of the form's first and last meridians: the columns of its
/// net at the sweep's two ends (the sweep is along u, or along v with `swap_uv`). A clamped
/// end's column is that boundary curve's own control polygon, and a meridian lies in a plane
/// through the axis, so every one of its control points has the meridian's angle about the
/// axis.
fn meridians(srf: &BSplineSurface, swap_uv: bool) -> Result<[Vec<Point3>; 2]> {
let nurb = srf
.gw_nurb()
.ok_or_else(|| SolidError::null_pointer(OP_NURB_TORUS))?;
let (n, m) = (nurb.n(), nurb.m());
let column = |end: bool| -> Result<Vec<Point3>> {
if swap_uv {
let j = if end { m } else { 0 };
(0..=n).map(|i| Ok(nurb.cpt(i, j)?.to_euclid())).collect()
} else {
let i = if end { n } else { 0 };
(0..=m).map(|j| Ok(nurb.cpt(i, j)?.to_euclid())).collect()
}
};
Ok([column(false)?, column(true)?])
}
/// The directions, about the axis, of the fitted first sweep row's ends (`is_arc`'s frame and
/// angles): [`sweep_frame`] reads only their sides from them.
fn fitted_sides(frame: &Axis2Placement, angles: Extent1d) -> [Vec3; 2] {
let at = |deg: f64| {
let a = deg * PI / 180.0;
a.cos() * frame.x_axis() + a.sin() * frame.y_axis()
};
[at(angles.min()), at(angles.max())]
}
/// Not upstream (#3199): a recognised torus's x axis, y axis and sweep interval (degrees) from
/// its form's first and last meridians (`meridians`: their control points, exact data), about
/// the axis through `origin` along the unit `z`. A meridian lies in a plane through the axis,
/// so its control points share its angle on the side the tube's centre is on, and the
/// opposite angle past the axis (a spindle torus's tube crosses it); `sides` (the fitted
/// row's end directions, [`fitted_sides`]) says which side that is, and nothing else. Each
/// meridian's angle is read at its control point farthest from the axis on that side: x is
/// the first meridian's direction square to the axis (so the interval starts at 0), y `z x
/// x`, and the interval's end the last meridian's angle from x about z (`atan2` in that
/// frame), a closed sweep (within `SM_EFF_ZERO_SQRT` of 0) at 360, as `is_arc` maps its end.
/// `is_arc` fitted the first sweep row's circle through three samples, so on a short arc its
/// centre, and with it the x axis and the end angle, was off the axis by its conditioning
/// (1e-14 rad on #3185's grooves); the converter centres the sweep circle on the axis, so its
/// ends and its inside now agree to rounding. `None` when a meridian has no control point off
/// the axis on its side.
fn sweep_frame(
origin: Point3,
z: Vec3,
sides: [Vec3; 2],
[first, last]: [Vec<Point3>; 2],
) -> Option<(Vec3, Vec3, Extent1d)> {
let radial = |p: &Point3| {
let s = *p - origin;
s - s.dot(z) * z
};
let farthest = |ps: &[Point3], side: Vec3| {
ps.iter()
.map(radial)
.filter(|r| r.dot(side) > 0.0)
.max_by(|a, b| a.length().total_cmp(&b.length()))
};
let [first_side, last_side] = sides;
let x = farthest(&first, first_side)?.unitize().ok()?;
let y = z.cross(x);
let e = farthest(&last, last_side)?;
let mut end_deg = e.dot(y).atan2(e.dot(x)) * 180.0 / PI;
if end_deg.abs() < EFF_ZERO_SQRT {
end_deg = 360.0;
} else if end_deg < 0.0 {
end_deg += 360.0;
}
Some((x, y, Extent1d::from_min_max(0.0, end_deg)))
}
fn iso_curve(
rev: &SurfOfRevolution,
shape: Shape,
@@ -0,0 +1,200 @@
//! #3144: whether two bodies meet, decided by a certified bracket of their boundary distance
//! (`query::contact`), against closed forms: boxes touching on a face, apart by 1e-9,
//! overlapping in a face, an edge resting on a face, interpenetrating, one inside the other,
//! far apart; spheres apart and crossing; a cylinder crossing a box's face and one against a
//! side along a line; a peg through a hole that it clears.
#![allow(clippy::unwrap_used, clippy::panic, clippy::float_cmp)] // test
use solidcore::boolean::api_brep::boolean_difference;
use solidcore::geom::certified::Verdict;
use solidcore::kernel::Kernel;
use solidcore::primitive::api_primitives::{create_box, create_cylinder, create_sphere};
use solidcore::query::contact::{boundary_distance, meets};
use solidcore::topology::brep::mirror::transform_matrix;
use solidcore::{Brep, Point3};
fn k() -> Kernel {
Kernel {
fixes: solidcore::kernel::UpstreamFixes::all(),
..Kernel::default()
}
}
fn cube(k: &mut Kernel, at: [f64; 3], size: f64) -> Brep {
create_box(k, Point3::new(at[0], at[1], at[2]), size, size, size).unwrap()
}
/// `b` turned by `angle` about the x axis, then moved by `by`.
fn turned_about_x(k: &mut Kernel, b: &mut Brep, angle: f64, by: [f64; 3]) {
let (c, s) = (angle.cos(), angle.sin());
// row vectors (`p' = p A + t`): A's rows are the images of x, y and z, t the fourth row
let m = [
[1.0, 0.0, 0.0, 0.0],
[0.0, c, s, 0.0],
[0.0, -s, c, 0.0],
[by[0], by[1], by[2], 1.0],
];
transform_matrix(k, b, &m, true).unwrap();
}
/// Asserts the verdict at `tol`, the bracket shown on failure.
fn verdict_is(k: &Kernel, a: &mut Brep, b: &mut Brep, tol: f64, want: Verdict) {
let c = meets(k, a, b, tol).unwrap();
assert!(c.lower <= c.upper, "{c:?}");
assert_eq!(c.verdict, want, "at {tol}: {c:?}");
}
/// The bracket of the boundary distance, asserted to hold `exact`.
fn holds(k: &Kernel, a: &mut Brep, b: &mut Brep, exact: f64) {
let c = boundary_distance(k, a, b, 1e-6).unwrap();
assert_eq!(c.verdict, Verdict::Yes, "{c:?}");
assert!(c.lower <= exact && exact <= c.upper, "{exact} not in {c:?}");
assert!(c.upper - c.lower <= 1e-6 * c.upper.max(1.0), "{c:?}");
}
#[test]
fn boxes_touching_on_a_face_meet_at_any_tolerance() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = cube(&mut k, [1.0, 0.0, 0.0], 1.0);
let c = meets(&k, &mut a, &mut b, 1e-12).unwrap();
assert_eq!(c.verdict, Verdict::Yes, "{c:?}");
let [p, q] = c.witness.unwrap();
assert!(
p.distance(q) <= 1e-12 && (p.x - 1.0).abs() <= 1e-12,
"{c:?}"
);
}
#[test]
fn boxes_apart_by_a_nanometre_meet_only_at_a_tolerance_past_it() {
let mut k = k();
// the gap as the doubles have it
let gap = (1.0 + 1e-9) - 1.0;
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = cube(&mut k, [1.0 + 1e-9, 0.0, 0.0], 1.0);
verdict_is(&k, &mut a, &mut b, 1e-8, Verdict::Yes);
verdict_is(&k, &mut a, &mut b, 1e-10, Verdict::No);
holds(&k, &mut a, &mut b, gap);
}
#[test]
fn boxes_overlapping_in_a_face_meet() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = cube(&mut k, [1.0, 0.5, 0.5], 1.0);
verdict_is(&k, &mut a, &mut b, 1e-12, Verdict::Yes);
}
#[test]
fn an_edge_resting_on_a_face_meets_it() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
// a cube centred on the x axis, turned 45 degrees about it (either way: it is square), is
// a diamond whose lowest edge is at z = -sqrt(2)/2; moved onto a's top face
let mut b = cube(&mut k, [0.0, -0.5, -0.5], 1.0);
let half = std::f64::consts::FRAC_1_SQRT_2;
turned_about_x(
&mut k,
&mut b,
std::f64::consts::FRAC_PI_4,
[0.0, 0.5, 1.0 + half],
);
let c = meets(&k, &mut a, &mut b, 1e-12).unwrap();
assert_eq!(c.verdict, Verdict::Yes, "{c:?}");
let [p, _] = c.witness.unwrap();
assert!((p.z - 1.0).abs() <= 1e-9, "{c:?}");
}
#[test]
fn interpenetrating_boxes_meet() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = cube(&mut k, [0.5; 3], 1.0);
verdict_is(&k, &mut a, &mut b, 1e-12, Verdict::Yes);
}
#[test]
fn a_box_inside_another_meets_it_and_their_walls_are_one_apart() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 3.0);
let mut b = cube(&mut k, [1.0; 3], 1.0);
verdict_is(&k, &mut a, &mut b, 1e-3, Verdict::Yes);
verdict_is(&k, &mut b, &mut a, 1e-3, Verdict::Yes);
holds(&k, &mut a, &mut b, 1.0);
}
#[test]
fn boxes_far_apart_do_not_meet_and_their_gap_is_bracketed() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = cube(&mut k, [5.0, 0.0, 0.0], 1.0);
verdict_is(&k, &mut a, &mut b, 0.1, Verdict::No);
holds(&k, &mut a, &mut b, 4.0);
}
#[test]
fn spheres_apart_are_their_centres_gap_less_their_radii() {
let mut k = k();
let mut a = create_sphere(&mut k, Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let mut b = create_sphere(&mut k, Point3::new(3.0, 0.0, 0.0), 1.0).unwrap();
verdict_is(&k, &mut a, &mut b, 0.5, Verdict::No);
verdict_is(&k, &mut a, &mut b, 2.0, Verdict::Yes);
holds(&k, &mut a, &mut b, 1.0);
}
#[test]
fn crossing_spheres_meet_where_their_surfaces_cross() {
let mut k = k();
let mut a = create_sphere(&mut k, Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
let mut b = create_sphere(&mut k, Point3::new(1.5, 0.0, 0.0), 1.0).unwrap();
let c = meets(&k, &mut a, &mut b, 1e-9).unwrap();
assert_eq!(c.verdict, Verdict::Yes, "{c:?}");
// the witness is on the circle where they cross: x = 0.75, |p| = 1
let [p, _] = c.witness.unwrap();
assert!((p.x - 0.75).abs() <= 1e-6, "{c:?}");
}
#[test]
fn a_cylinder_crossing_a_box_face_meets_the_box() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = create_cylinder(&mut k, Point3::new(0.5, 0.5, 0.5), 0.3, 1.0).unwrap();
verdict_is(&k, &mut a, &mut b, 1e-9, Verdict::Yes);
}
/// A cylinder touching a box side along a line meets it; moved 0.25 off, its bracket holds
/// 0.25 and the verdicts either side of it are exact.
///
/// The bracket is not 1e-6 wide here: trimmed rim pieces stall `lower` at 0.25 - 0.0013 (the
/// end caps' pieces at the rim straddle the circular trim, and each hull carries a sliver of
/// the cap's plane outside the disk toward the box; their subdivision runs out of pairs
/// first). The bracket contains the truth; verdicts outside [0.2487, 0.25] are exact. The
/// width asserted is what the pair budget certifies, not a target.
#[test]
fn a_cylinder_against_a_box_side_meets_it_along_a_line() {
let mut k = k();
let mut a = cube(&mut k, [0.0; 3], 1.0);
let mut b = create_cylinder(&mut k, Point3::new(1.5, 0.5, 0.0), 0.5, 1.0).unwrap();
verdict_is(&k, &mut a, &mut b, 1e-9, Verdict::Yes);
let mut far = create_cylinder(&mut k, Point3::new(1.75, 0.5, 0.0), 0.5, 1.0).unwrap();
verdict_is(&k, &mut a, &mut far, 0.24, Verdict::No);
verdict_is(&k, &mut a, &mut far, 0.26, Verdict::Yes);
let c = boundary_distance(&k, &mut a, &mut far, 1e-6).unwrap();
assert!(c.lower <= 0.25 && 0.25 <= c.upper, "{c:?}");
assert!(c.lower >= 0.2487 && c.upper - 0.25 <= 1e-12, "{c:?}");
}
#[test]
fn a_peg_through_a_hole_it_clears_does_not_meet_the_plate() {
let mut k = k();
// a 4 x 4 x 1 plate with a hole of radius 1 through its middle, and a peg of radius 0.75
// down the hole's axis: the peg clears the hole's wall by 0.25
let plate = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 4.0, 4.0, 1.0).unwrap();
let hole = create_cylinder(&mut k, Point3::new(2.0, 2.0, -1.0), 1.0, 3.0).unwrap();
let mut plate = boolean_difference(&mut k, plate, hole).unwrap();
let mut peg = create_cylinder(&mut k, Point3::new(2.0, 2.0, -2.0), 0.75, 5.0).unwrap();
verdict_is(&k, &mut plate, &mut peg, 0.1, Verdict::No);
holds(&k, &mut plate, &mut peg, 0.25);
}
+11
View File
@@ -126,6 +126,17 @@ give the same class, and the net lies on that class's surface derived from the s
parametrisation's: the net reversed, swapped, split or trimmed is still on S, so those edits
carry it.
A round of the `Circle` class is certified on its own torus. The class is read within the body's
tolerance, so supports only that close to it (a plane 1e-13 off square to a cylinder's axis, as a
record can carry them) have no torus that touches both exactly, and the round the solver builds,
tangent to the supports as they are, lies on a torus of its own. Its definition is that torus: the
torus of the net's own form (`own_torus`: a revolution's axis and generating arc, or a torus's
label), which the form is proved to lie on (`lies_on`), at bound 0; and the distance from it to the
class's torus derived from the supports, stated (`ExactRound::supports_distance`: the axes' angle
times the reach, the centres' distance and the radii's differences) and refused past the tolerance
the class was read within. Supports exactly of the class give a distance at rounding. The supports
are never moved to fit.
### 3.4 The blend definition
`ProceduralBlend` holds the net (`net`, a `BSplineSurface`) and the definition