mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 492c750122 |
+32
-28
@@ -303,8 +303,8 @@ pub fn create_draft_sweep(
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for no path curve, a radius `<= 0` (a NaN radius passes, as
|
||||
/// upstream), or a radius that reaches the centre of a bend in the path
|
||||
/// ([`path_bends_within`]; not upstream, which builds that pipe with its wall folded
|
||||
/// upstream), or a radius found to reach the centre of a bend in the path
|
||||
/// ([`bend_found_within`]; not upstream, which builds that pipe with its wall folded
|
||||
/// through itself, WR-774); [`SolidError::Unported`] otherwise
|
||||
/// (`SmBrep::SmBrep`, until the B-rep constructor is ported).
|
||||
pub fn create_pipe_sweep(
|
||||
@@ -322,7 +322,9 @@ pub fn create_pipe_sweep(
|
||||
}
|
||||
|
||||
let path = bspline_of(path_curve, OP_PIPE)?;
|
||||
if path_bends_within(&path, pipe_radius) {
|
||||
// Refusal only: a bend found refuses the pipe; none found proves nothing, and the pipe
|
||||
// is built unchecked, as upstream builds it (definitions-and-certificates.md §7.6).
|
||||
if bend_found_within(&path, pipe_radius).is_some() {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_PIPE,
|
||||
"the pipe radius reaches the centre of a bend in the path, so the pipe's wall \
|
||||
@@ -348,45 +350,46 @@ pub fn create_pipe_sweep(
|
||||
})
|
||||
}
|
||||
|
||||
/// The samples per knot span, both span ends included, at which [`path_bends_within`]
|
||||
/// measures a pipe path's curvature. A span is one polynomial or rational piece, whose
|
||||
/// The samples per knot span, both span ends included, at which [`bend_found_within`]
|
||||
/// looks for a bend tighter than a pipe. A span is one polynomial or rational piece, whose
|
||||
/// curvature rises and falls only a few times, so 16 samples find a bend well inside the
|
||||
/// pipe at a cost that is small beside the sweep itself. A bend tighter than the pipe that
|
||||
/// lies between two samples is missed, and that pipe is built as before.
|
||||
/// lies between two samples is not found, and that pipe is built as before.
|
||||
const PIPE_BEND_SAMPLES_PER_SPAN: u32 = 16;
|
||||
|
||||
/// Whether `path` bends somewhere with a radius of at most `radius`, that is, with
|
||||
/// curvature at least `1 / radius`. A pipe of that radius folds its wall through itself on
|
||||
/// the inside of such a bend: the circles of neighbouring sections cross there, so the
|
||||
/// swept surface is singular (Maekawa, Patrikalakis, Sakkalis and Yu, "Analysis and
|
||||
/// applications of pipe surfaces", CAGD 15(5), 1998).
|
||||
/// A parameter at which `path` bends with a radius of at most `radius`, that is, with
|
||||
/// curvature at least `1 / radius`, if one is found. A pipe of that radius folds its wall
|
||||
/// through itself on the inside of such a bend: the circles of neighbouring sections cross
|
||||
/// there, so the swept surface is singular (Maekawa, Patrikalakis, Sakkalis and Yu,
|
||||
/// "Analysis and applications of pipe surfaces", CAGD 15(5), 1998).
|
||||
///
|
||||
/// Measured at [`PIPE_BEND_SAMPLES_PER_SPAN`] samples per knot span. A `true` is proved by
|
||||
/// the sample that found the bend; a `false` only says that no sample found one. A sample
|
||||
/// whose curvature cannot be computed (a zero-length tangent) proves nothing and is
|
||||
/// skipped. A pipe that crosses itself between distant parts of its path is not found
|
||||
/// here.
|
||||
fn path_bends_within(path: &BSplineCurve, radius: f64) -> bool {
|
||||
let Some(nurb) = path.nurb.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
/// **Refusal only.** The path's curvature is evaluated at [`PIPE_BEND_SAMPLES_PER_SPAN`]
|
||||
/// samples per knot span. A `Some` is proved by the sample that found the bend, so it may
|
||||
/// refuse a pipe. A `None` proves nothing: it is not a certificate that the path bends no
|
||||
/// tighter than `radius` anywhere (definitions-and-certificates.md §7.6, "a check at sample
|
||||
/// points proves nothing"), and no caller may accept a construction on it. A certified
|
||||
/// check, a lower bound on the radius of curvature over each knot span enclosed on the
|
||||
/// Bernstein form of the curvature's numerator and denominator, is open work (#15038).
|
||||
/// A sample whose curvature cannot be computed (a zero-length tangent) proves nothing and
|
||||
/// is skipped. A pipe that crosses itself between distant parts of its path is not looked
|
||||
/// for here.
|
||||
fn bend_found_within(path: &BSplineCurve, radius: f64) -> Option<f64> {
|
||||
let nurb = path.nurb.as_ref()?;
|
||||
// A clamped curve's spans run between knots[degree] and knots[n + 1].
|
||||
let Some(span_knots) = nurb.knots().get(nurb.degree()..=nurb.n() + 1) else {
|
||||
return false;
|
||||
};
|
||||
let span_knots = nurb.knots().get(nurb.degree()..=nurb.n() + 1)?;
|
||||
let reaches = |u: f64, side: Side| {
|
||||
nurb.eval_curvature(u, side, false)
|
||||
.is_ok_and(|c| c.kappa * radius >= 1.0)
|
||||
};
|
||||
span_knots.windows(2).any(|ends| {
|
||||
span_knots.windows(2).find_map(|ends| {
|
||||
let &[start, end] = ends else {
|
||||
return false;
|
||||
return None;
|
||||
};
|
||||
// A repeated knot spans nothing.
|
||||
if end <= start {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
(0..=PIPE_BEND_SAMPLES_PER_SPAN).any(|i| {
|
||||
(0..=PIPE_BEND_SAMPLES_PER_SPAN).find_map(|i| {
|
||||
let at = f64::from(i) / f64::from(PIPE_BEND_SAMPLES_PER_SPAN);
|
||||
// At its end knot a span is evaluated from its own side, not the next span's.
|
||||
let side = if i == PIPE_BEND_SAMPLES_PER_SPAN {
|
||||
@@ -394,7 +397,8 @@ fn path_bends_within(path: &BSplineCurve, radius: f64) -> bool {
|
||||
} else {
|
||||
Side::Left
|
||||
};
|
||||
reaches(start + at * (end - start), side)
|
||||
let u = start + at * (end - start);
|
||||
reaches(u, side).then_some(u)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+30
-24
@@ -106,28 +106,13 @@ const FIT: &[(f64, f64, f64)] = &[
|
||||
/// The pipe's radius: the wire's 1.5 mm circle.
|
||||
const RADIUS: f64 = 0.75;
|
||||
|
||||
/// The pipe's radius along [`spline_path`]. The fitted spline overshoots where a straight
|
||||
/// meets a round near the wire's far end, bending there with radius about 0.564 (sampled at
|
||||
/// (168.51, -37.90, 50.71)). A pipe of [`RADIUS`] would fold its wall through itself in that
|
||||
/// bend, and `create_pipe_sweep` refuses it (#13766), so this pipe stays inside the bend.
|
||||
const SPLINE_RADIUS: f64 = 0.5;
|
||||
|
||||
/// The balance wire's points fitted open, as the wire runs. Fitted closed (C1 between its
|
||||
/// two ends, though the wire's ends are apart), the spline bends with radius about 0.041
|
||||
/// near its start and 0.045 near its end, which no pipe of the wire's size fits.
|
||||
/// The balance wire's points fitted by a cubic spline closed (C1 between its two ends), as
|
||||
/// the case was written.
|
||||
fn spline_path() -> Curve {
|
||||
let pts: Vec<Point3> = FIT.iter().map(|&(x, y, z)| Point3::new(x, y, z)).collect();
|
||||
Curve::from(
|
||||
BSplineCurve::interpolate_points(
|
||||
&pts,
|
||||
None,
|
||||
3,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
ParamMethod::ChordLength,
|
||||
)
|
||||
.unwrap(),
|
||||
BSplineCurve::interpolate_points(&pts, None, 3, None, None, true, ParamMethod::ChordLength)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -657,10 +642,6 @@ fn issue_2572_every_path_wall_is_a_definition_or_in_the_ledger() {
|
||||
"path-b-spline",
|
||||
create_pipe_sweep(&mut k, RADIUS, Some(&arc_path()), false).unwrap(),
|
||||
),
|
||||
(
|
||||
"path-spline",
|
||||
create_pipe_sweep(&mut k, SPLINE_RADIUS, Some(&spline_path()), false).unwrap(),
|
||||
),
|
||||
(
|
||||
"path-b-spline",
|
||||
create_pipe_sweep(&mut k, RADIUS, Some(&wire), false).unwrap(),
|
||||
@@ -730,13 +711,38 @@ fn issue_2572_every_path_wall_is_a_definition_or_in_the_ledger() {
|
||||
unlisted.is_empty(),
|
||||
"bare wall faces of kinds the ledger does not list: {unlisted:?}"
|
||||
);
|
||||
let fixed: Vec<&String> = ledger.keys().filter(|kd| !bare.contains(*kd)).collect();
|
||||
// `path-spline` is the known-failing case's (`issue_2572_the_balance_wire_spline_pipe_is_in_the_ledger`).
|
||||
let fixed: Vec<&String> = ledger
|
||||
.keys()
|
||||
.filter(|kd| *kd != "path-spline" && !bare.contains(*kd))
|
||||
.collect();
|
||||
assert!(
|
||||
fixed.is_empty(),
|
||||
"listed kinds no case makes bare any more (fixed? delete their ledger files): {fixed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The ledger's `path-spline` kind: the wire's pipe along its points fitted by a spline.
|
||||
/// Known failing since #14640: that spline, fitted closed (C1 between the wire's two ends,
|
||||
/// though they are apart), bends with radius about 0.041 near its start and 0.045 near its
|
||||
/// end, so `create_pipe_sweep` refuses the wire's 0.75 pipe as folding through itself. The
|
||||
/// input is the case's own and is not changed to pass (#15038): the case needs a path the
|
||||
/// wire's pipe fits, and until it has one this test stays listed as failing.
|
||||
#[test]
|
||||
#[ignore = "known failing: the closed spline fit bends with radius 0.041 < 0.75 and the pipe is refused (#14640, #15038)"]
|
||||
fn issue_2572_the_balance_wire_spline_pipe_is_in_the_ledger() {
|
||||
let mut k = Kernel::default();
|
||||
assert!(path_ledger().contains_key("path-spline"));
|
||||
let b = create_pipe_sweep(&mut k, RADIUS, Some(&spline_path()), false).unwrap();
|
||||
let bare = walls(&b)
|
||||
.iter()
|
||||
.any(|s| s.procedural_definition().is_none() && !matches!(s.kind(), SurfaceKind::Plane(_)));
|
||||
assert!(
|
||||
bare,
|
||||
"the listed kind no longer appears (fixed? delete its ledger file)"
|
||||
);
|
||||
}
|
||||
|
||||
/// The definition's partials at the ends of the net's parameter across the sweep are those
|
||||
/// of its points, one-sided: the held parameter keeps the end segment's slope.
|
||||
#[test]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
use solidcore::blend::feature_executive::{FeatureExecutive, RebuildBrepType};
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::mass::enclosure::VolumeSign;
|
||||
use solidcore::mass::enclosure::{enclose_volume, VolumeSign};
|
||||
use solidcore::nurbs::nurbs_srf::decompose_srf;
|
||||
use solidcore::surface::extend::Continuity;
|
||||
use solidcore::surface::side::BoxSide;
|
||||
@@ -143,13 +143,16 @@ fn the_extrusion_defeatures_to_the_same_solid_at_any_reach() {
|
||||
assert!(s.manifold, "reach {dist:?}");
|
||||
assert!(s.check.lines().is_empty(), "reach {dist:?}: {:?}", s.check);
|
||||
assert_eq!(bp1::faces(&b, None).unwrap().len(), 13, "reach {dist:?}");
|
||||
Some(positive(s.volume))
|
||||
positive(s.volume);
|
||||
Some(narrow(&k, &mut b))
|
||||
};
|
||||
let Some(v) = base(None) else { return };
|
||||
// the same solid at every reach: the volume enclosures meet
|
||||
// the same solid at every reach: every volume in one enclosure is within `REL` of
|
||||
// every volume in the other
|
||||
for d in [10.0, 15.0, 19.999_999_999, 20.0, 25.0] {
|
||||
let w = base(Some(d)).unwrap();
|
||||
assert!(w.0 <= v.1 && v.0 <= w.1, "reach {d}: {w:?} against {v:?}");
|
||||
let apart = (w.1 - v.0).max(v.1 - w.0);
|
||||
assert!(apart <= REL * v.0, "reach {d}: {w:?} against {v:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +193,37 @@ fn a_feature_whose_cap_cannot_be_built_is_kept_and_named() {
|
||||
let s = validation::soundness(&mut k, &mut b).unwrap();
|
||||
assert!(s.manifold);
|
||||
assert!(s.check.lines().is_empty(), "{:?}", s.check);
|
||||
let (lo, hi) = positive(s.volume);
|
||||
positive(s.volume);
|
||||
// every volume in the enclosure is within `REL` of the body's
|
||||
let (lo, hi) = narrow(&k, &mut b);
|
||||
let apart = (hi - VOLUME).max(VOLUME - lo);
|
||||
assert!(apart <= REL * lo, "[{lo}, {hi}]");
|
||||
}
|
||||
|
||||
/// The volume of the body `a_feature_whose_cap_cannot_be_built_is_kept_and_named` makes: the
|
||||
/// middle of its certified enclosure `[757.499_497_550_496_5, 757.499_497_564_175]` (width
|
||||
/// 1.4e-8). The case first pinned `757.499_499_825_162`, the volume estimator's answer at
|
||||
/// accuracy 1e-6 (`brep_volume_default`, still its answer on main); the enclosure proves that
|
||||
/// 3.0e-9 of the volume too large, past the case's own relative bound [`REL`] (#15038).
|
||||
const VOLUME: f64 = 757.499_497_557;
|
||||
|
||||
/// How far apart, relative to the volume, two volumes of the same solid may be: the bound
|
||||
/// the case compared its volumes with before the volume became an enclosure (#15038).
|
||||
const REL: f64 = 1e-9;
|
||||
|
||||
/// The body's volume enclosed to a quarter of [`REL`] of itself, as `(lo, hi)`: narrow
|
||||
/// enough that two enclosures of one solid are within `REL` of each other end to end, so the
|
||||
/// comparison is the relative bound on numbers, proved, and not an overlap of enclosures of
|
||||
/// any width.
|
||||
fn narrow(k: &Kernel, b: &mut Brep) -> (f64, f64) {
|
||||
let rough = enclose_volume(k, b, 1.0).unwrap();
|
||||
assert!(rough.lo > 0.0, "{rough:?}");
|
||||
let e = enclose_volume(k, b, 0.25 * REL * rough.lo).unwrap();
|
||||
assert!(
|
||||
lo <= 757.499_499_825_162 && 757.499_499_825_162 <= hi,
|
||||
"[{lo}, {hi}]"
|
||||
e.width() <= 0.25 * REL * rough.lo,
|
||||
"not enclosed narrowly enough: {e:?}"
|
||||
);
|
||||
(e.lo, e.hi)
|
||||
}
|
||||
|
||||
/// A volume proved positive, as its enclosure's ends.
|
||||
|
||||
Reference in New Issue
Block a user